Files
erv-clientright/n8n.js
2026-03-13 10:42:01 +03:00

2174 lines
92 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Code node: Mini-app "Подтверждение данных" — фикс без вложенных ${} в шаблоне
// --- ВХОД ---
const raw = $input.all()?.[0]?.json ?? {};
// Извлекаем SMS данные из входящих данных n8n
const smsInputData = {
prefix: raw.prefix || '',
session_token: raw.session_token || '',
telegram_id: raw.telegram_id || '',
claim_id: raw.claim_id || '',
unified_id: (raw.propertyName && raw.propertyName.meta && raw.propertyName.meta.unified_id) || '',
user_id: (raw.propertyName && raw.propertyName.meta && raw.propertyName.meta.user_id) || '',
status: (raw.propertyName && raw.propertyName.meta && raw.propertyName.meta.status) || ''
};
console.log('SMS input data from n8n:', smsInputData);
// --- Утилиты ---
function safeGet(...keys) {
for (const k of keys) {
if (k === undefined || k === null) continue;
if (typeof k === 'object') { if (Object.keys(k).length) return k; continue; }
const s = String(k).trim(); if (s !== '') return k;
}
return '';
}
function tryParseJSON(x) { try { return typeof x === 'string' ? JSON.parse(x) : x; } catch { return null; } }
function escapeHtml(str){
if (str === undefined || str === null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
// Нормализация денежной суммы из любого формата в число
function normalizeMoney(rawValue) {
if (!rawValue) return null;
console.log('normalizeMoney: входящее значение:', rawValue, 'тип:', typeof rawValue);
var s = String(rawValue);
// Удаляем все лишнее: пробелы, слова "рублей", "руб", валютные знаки
s = s.replace(/\s+/g, ''); // убираем пробелы
s = s.replace(/руб(лей|ль|\.)?/gi, ''); // убираем "рублей", "руб", "рубль"
s = s.replace(/₽|р\.|р$/gi, ''); // убираем символы валют
s = s.replace(/[^0-9,.-]/g, ''); // оставляем только цифры, запятые, точки, минусы
// Заменяем запятую на точку для десятичной части
s = s.replace(',', '.');
console.log('normalizeMoney: после очистки:', s);
// Проверяем, что получилось валидное число
if (!/^-?[0-9]+(\.[0-9]{1,2})?$/.test(s)) {
console.log('normalizeMoney: невалидный формат после очистки');
return null;
}
var result = parseFloat(s);
console.log('normalizeMoney: результат:', result);
return result > 0 ? result : null;
}
// --- Достаём объект кейса из «типичных» мест ---
let dataCandidate = null;
if (!dataCandidate && raw.propertyName !== undefined) {
// Если propertyName - это объект (как в вашем случае), берем его напрямую
if (typeof raw.propertyName === 'object' && raw.propertyName !== null) {
dataCandidate = raw.propertyName;
} else if (typeof raw.propertyName === 'string') {
dataCandidate = tryParseJSON(raw.propertyName);
}
}
if (!dataCandidate && raw.value !== undefined) dataCandidate = tryParseJSON(raw.value);
if (!dataCandidate && (raw.user || raw.project || raw.offenders || raw.meta)) dataCandidate = raw;
if (!dataCandidate && raw.data) dataCandidate = raw.data;
if (!dataCandidate && raw.output) dataCandidate = tryParseJSON(raw.output) || raw.output;
dataCandidate = dataCandidate || {};
console.log('Raw dataCandidate:', dataCandidate);
console.log('Type of dataCandidate:', typeof dataCandidate);
console.log('Keys of dataCandidate:', Object.keys(dataCandidate || {}));
// --- Базовые схемы ---
const baseUser = { firstname:null, secondname:null, lastname:null, mobile:null, email:null, tgid:null, birthday:null, birthplace:null, mailingstreet:null, inn:null };
const baseProject = { category:null, direction:null, agrprice:null, subject:null, agrdate:null, startdate:null, finishdate:null, period_text:null, description:null, reason:null };
const baseOffender = { accountname:null, address:null, email:null, website:null, phone:null, inn:null, ogrn:null };
// --- Нормализуем данные под новую структуру ---
function normalizeData(data) {
console.log('=== НОРМАЛИЗАЦИЯ ДАННЫХ ===');
console.log('Input data:', data);
console.log('Has propertyName:', !!data.propertyName);
console.log('Has applicant in propertyName:', !!(data.propertyName && data.propertyName.applicant));
// Если данные приходят в новом формате (с propertyName)
if (data.propertyName && data.propertyName.applicant) {
console.log('Using NEW format with propertyName');
var props = data.propertyName;
var applicant = props.applicant || {};
var caseData = props.case || {};
var contract = props.contract_or_service || {};
var offenders = props.offenders || [];
var claim = props.claim || {};
var meta = props.meta || {};
console.log('=== ОТЛАДКА КОНТРАКТА ===');
console.log('contract_or_service:', contract);
console.log('subject:', contract.subject);
console.log('agreement_date_fmt:', contract.agreement_date_fmt);
console.log('agreement_date:', contract.agreement_date);
console.log('period_start_fmt:', contract.period_start_fmt);
console.log('period_end_fmt:', contract.period_end_fmt);
// Получаем список приложенных документов
var attachments = props.attachments_names || [];
console.log('=== ОТЛАДКА ПРИЛОЖЕНИЙ ===');
console.log('attachments_names:', attachments);
return {
user: {
firstname: applicant.first_name || null,
secondname: applicant.middle_name || null,
lastname: applicant.last_name || null,
mobile: applicant.phone || null,
email: applicant.email || null,
birthday: applicant.birth_date_fmt || applicant.birth_date || null,
birthplace: applicant.birth_place || null,
mailingstreet: applicant.address || null,
inn: applicant.inn || null,
tgid: null
},
project: {
category: caseData.category || null, // Тема обращения - общая категория (только для чтения)
direction: caseData.direction || null,
agrprice: normalizeMoney(contract.amount_paid_fmt || contract.amount_paid) || null,
subject: contract.subject || null,
agrdate: contract.agreement_date_fmt || contract.agreement_date || null,
startdate: contract.period_start_fmt || contract.period_start || null,
finishdate: contract.period_end_fmt || contract.period_end || null,
period_text: contract.period_text || null,
description: claim.description || null,
reason: claim.reason || caseData.category || null // Копируется из категории, но редактируемая
},
attachments: attachments, // Список приложенных документов
offenders: offenders.map(function(o) {
return {
accountname: o.name || null,
address: o.address || null,
email: o.email || null,
website: o.website || null,
phone: o.phone || null,
inn: o.inn || null,
ogrn: o.ogrn || null
};
}),
meta: Object.assign({}, meta, {
// Добавляем SMS данные из корня элемента массива
session_token: data.session_token || meta.claim_id || null,
prefix: data.prefix || null,
telegram_id: data.telegram_id || null,
claim_id: data.claim_id || meta.claim_id || null,
unified_id: meta.unified_id || null,
user_id: meta.user_id || null
})
};
}
// Если данные приходят в старом формате (прямо applicant, case, etc)
if (data.applicant || data.case || data.contract_or_service) {
var applicant = data.applicant || {};
var caseData = data.case || {};
var contract = data.contract_or_service || {};
var offenders = data.offenders || [];
var claim = data.claim || {};
console.log('=== ОТЛАДКА КОНТРАКТА (старый формат) ===');
console.log('contract_or_service:', contract);
console.log('subject:', contract.subject);
console.log('agreement_date_fmt:', contract.agreement_date_fmt);
return {
user: {
firstname: applicant.first_name || null,
secondname: applicant.middle_name || null,
lastname: applicant.last_name || null,
mobile: applicant.phone || null,
email: applicant.email || null,
birthday: applicant.birth_date_fmt || applicant.birth_date || null,
birthplace: applicant.birth_place || null,
mailingstreet: applicant.address || null,
inn: applicant.inn || null,
tgid: null
},
project: {
category: caseData.category || null,
direction: caseData.direction || null,
agrprice: normalizeMoney(contract.amount_paid_fmt || contract.amount_paid) || null,
subject: contract.subject || null,
agrdate: contract.agreement_date_fmt || contract.agreement_date || null,
startdate: contract.period_start_fmt || contract.period_start || null,
finishdate: contract.period_end_fmt || contract.period_end || null,
period_text: contract.period_text || null,
description: claim.description || null,
reason: claim.reason || caseData.category || null
},
attachments: data.attachments_names || [], // Список приложенных документов (старый формат)
offenders: offenders.map(function(o) {
return {
accountname: o.name || null,
address: o.address || null,
email: o.email || null,
website: o.website || null,
phone: o.phone || null,
inn: o.inn || null,
ogrn: o.ogrn || null
};
}),
meta: data.meta || {}
};
}
// Старый формат (обратная совместимость)
return {
user: Object.assign({}, baseUser, tryParseJSON(data.user) || data.user || {}),
project: Object.assign({}, baseProject, tryParseJSON(data.project) || data.project || {}),
offenders: Array.isArray(data.offenders) ? data.offenders.map(function(o) {
return Object.assign({}, baseOffender, o || {});
}) : [],
meta: Object.assign({}, data.meta || {})
};
}
// Если dataCandidate - массив, берем первый элемент
var dataToNormalize = Array.isArray(dataCandidate) ? dataCandidate[0] : dataCandidate;
console.log('Data to normalize:', dataToNormalize);
const caseObj = normalizeData(dataToNormalize);
if (!caseObj.offenders.length) caseObj.offenders = [ Object.assign({}, baseOffender) ];
console.log('Normalized caseObj:', caseObj);
// --- Куда постить подтверждение ---
const webhookUrl = String(safeGet(
raw.webhook_url, raw.confirm_url, raw.CONFIRM_URL, raw.WIZARD_POST_URL,
raw.headers?.['x-webhook-url'], raw.headers?.['x-confirm-url'], ''
)) || 'https://n8n.clientright.pro/webhook/miniapp/confirm';
// --- Сервисные поля (для отправки вместе с формой) ---
const sessionToken = String(safeGet(caseObj.meta?.session_token, raw.session_token, raw.query?.session_token, ''));
const telegramId = String(safeGet(caseObj.user?.tgid, raw.telegram_id, raw.query?.telegram_id, raw.tg_id, raw.chat_id, ''));
// Дополнительные поля для SMS - берем из входящих данных n8n
const smsMetaData = {
session_token: String(safeGet(smsInputData.session_token, sessionToken, '')),
prefix: String(safeGet(smsInputData.prefix, '')),
telegram_id: String(safeGet(smsInputData.telegram_id, telegramId, '')),
claim_id: String(safeGet(smsInputData.claim_id, '')),
unified_id: String(safeGet(smsInputData.unified_id, '')),
user_id: String(safeGet(smsInputData.user_id, ''))
};
console.log('dataToNormalize keys:', Object.keys(dataToNormalize || {}));
console.log('dataToNormalize.prefix:', dataToNormalize.prefix);
console.log('dataToNormalize.session_token:', dataToNormalize.session_token);
// Читаем дополнительные данные из URL параметров
try {
const urlParams = new URLSearchParams(window.location.search);
var urlMetaData = {
session_token: urlParams.get('session_token') || '',
prefix: urlParams.get('prefix') || '',
telegram_id: urlParams.get('telegram_id') || urlParams.get('tg_id') || '',
claim_id: urlParams.get('claim_id') || ''
};
console.log('URL search string:', window.location.search);
console.log('URL meta data:', urlMetaData);
} catch (e) {
console.error('Error reading URL params:', e);
var urlMetaData = {};
}
console.log('SMS meta data extracted:', smsMetaData);
// --- Безопасно встраиваем данные в HTML ---
let caseJson = JSON.stringify({
case: caseObj,
session_token: sessionToken,
telegram_id: telegramId,
token: raw.token || '', // Добавляем token для отправки формы
sms_meta: Object.assign({}, smsMetaData, urlMetaData) // Объединяем данные из JSON и URL
});
caseJson = caseJson.replace(/</g, '\\u003c'); // защита </script>
// --- HTML (без вложенных ${} в скрипте) ---
const html = `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Подтверждение данных</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
*{box-sizing:border-box}
body{
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
margin:0;padding:0;min-height:100vh;
color:#1f2937;
}
.wrap{
max-width:1400px;margin:0 auto;padding:20px;
min-height:100vh;display:flex;flex-direction:column;
}
.header{
text-align:center;margin-bottom:30px;color:white;
}
.header h1{
font-size:28px;font-weight:700;margin:0 0 8px;
text-shadow:0 2px 4px rgba(0,0,0,0.3);
}
.header p{
font-size:16px;opacity:0.9;margin:0;
}
.grid{
display:grid;grid-template-columns:1fr;gap:24px;flex:1;
}
@media(min-width:1200px){.grid{grid-template-columns:1fr 1fr}}
.card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(10px);
border-radius:20px;
box-shadow:0 20px 40px rgba(0,0,0,0.1);
padding:24px;
border:1px solid rgba(255,255,255,0.2);
transition:all 0.3s ease;
}
.card:hover{
transform:translateY(-2px);
box-shadow:0 25px 50px rgba(0,0,0,0.15);
}
.section{
margin-bottom:32px;
}
.section:last-child{margin-bottom:0}
.section-title{
font-size:20px;font-weight:600;margin:0 0 20px;
color:#1f2937;
display:flex;align-items:center;gap:8px;
}
.section-title::before{
content:'';width:4px;height:20px;
background:linear-gradient(135deg,#667eea,#764ba2);
border-radius:2px;
}
.statement-container{
background:#fff;border-radius:16px;padding:32px;
box-shadow:0 4px 20px rgba(0,0,0,0.08);
line-height:1.8;font-size:15px;
max-width:800px;margin:0 auto;
}
.statement-text{
font-family:'Times New Roman',serif;
text-align:justify;
}
.inline-field{
display:inline-block;min-width:120px;max-width:300px;
border:2px solid #e5e7eb;border-radius:6px;
padding:4px 8px;margin:0 2px;background:#fff;
font-size:inherit;font-family:inherit;
transition:all 0.2s ease;
vertical-align:baseline;
}
.inline-field:focus{
outline:none;border-color:#667eea;
box-shadow:0 0 0 2px rgba(102,126,234,0.1);
background:#f8fafc;
}
.inline-field:hover{border-color:#d1d5db}
.inline-field.large{
min-width:200px;max-width:500px;
}
.inline-field.full-width{
display:block;width:100%;min-width:auto;max-width:none;
margin:8px 0;padding:8px 12px;
}
.inline-field.required-empty{
border-color:#ef4444 !important;
background-color:#fef2f2 !important;
box-shadow:0 0 0 2px rgba(239,68,68,0.1) !important;
}
.inline-field.valid{
border-color:#10b981 !important;
background-color:#f0fdf4 !important;
}
.inline-field.verified{
border-color:#3b82f6 !important;
background-color:#eff6ff !important;
}
.date-field{
min-width:140px !important;
max-width:160px !important;
cursor:pointer;
}
.date-field::-webkit-calendar-picker-indicator{
cursor:pointer;
padding:2px;
border-radius:4px;
transition:background-color 0.2s ease;
}
.date-field::-webkit-calendar-picker-indicator:hover{
background-color:rgba(102,126,234,0.1);
}
.select-field{
min-width:160px !important;
max-width:300px !important;
cursor:pointer;
background-color:white;
background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position:right 8px center;
background-repeat:no-repeat;
background-size:16px 16px;
padding-right:32px !important;
appearance:none;
}
.select-field:focus{
background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23667eea' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
}
.readonly-field{
background-color:#f9fafb !important;
border-color:#d1d5db !important;
color:#6b7280 !important;
cursor:not-allowed !important;
font-weight:500;
}
.readonly-field:hover{
border-color:#d1d5db !important;
}
.readonly-field:focus{
outline:none !important;
border-color:#d1d5db !important;
box-shadow:none !important;
}
.checkbox-container{
display:flex;
align-items:flex-start;
gap:8px;
cursor:pointer;
padding:12px;
margin:8px 0;
border-radius:8px;
transition:background-color 0.2s ease;
}
.checkbox-container:hover{
background-color:rgba(102,126,234,0.05);
}
.checkbox-container.required-checkbox{
border:2px solid #e5e7eb;
}
.checkbox-container.required-checkbox.error{
border-color:#ef4444;
background-color:#fef2f2;
}
.checkbox-field{
width:18px;
height:18px;
margin:0;
cursor:pointer;
accent-color:#667eea;
}
.checkmark{
width:18px;
height:18px;
border:2px solid #d1d5db;
border-radius:4px;
position:relative;
transition:all 0.2s ease;
flex-shrink:0;
}
.checkbox-field:checked + .checkmark{
background-color:#667eea;
border-color:#667eea;
}
.checkbox-field:checked + .checkmark::after{
content:'✓';
position:absolute;
color:white;
font-size:12px;
font-weight:bold;
left:50%;
top:50%;
transform:translate(-50%, -50%);
}
.checkbox-field{
position:absolute;
opacity:0;
cursor:pointer;
}
.checkbox-label{
font-size:14px;
line-height:1.5;
color:#374151;
cursor:pointer;
}
.checkbox-label a{
color:#667eea;
text-decoration:none;
}
.checkbox-label a:hover{
text-decoration:underline;
}
.field-hint{
font-size:11px;color:#6b7280;font-style:italic;
margin-left:4px;
}
.validation-message{
font-size:12px;margin-top:4px;padding:4px 8px;
border-radius:6px;display:none;
}
.validation-message.error{
color:#dc2626;background:#fef2f2;border:1px solid #fecaca;
}
.validation-message.success{
color:#059669;background:#ecfdf5;border:1px solid #a7f3d0;
}
.section-break{
margin:24px 0;border-top:1px solid #e5e7eb;
padding-top:16px;
}
.verification-progress{
position:fixed;top:20px;right:20px;
background:rgba(255,255,255,0.95);backdrop-filter:blur(10px);
border-radius:12px;padding:12px 16px;
box-shadow:0 4px 20px rgba(0,0,0,0.1);
border:1px solid rgba(255,255,255,0.2);
z-index:1000;
}
.progress-bar{
width:200px;height:4px;background:#e5e7eb;
border-radius:2px;margin:8px 0;
overflow:hidden;
}
.progress-fill{
height:100%;background:linear-gradient(135deg,#667eea,#764ba2);
border-radius:2px;transition:width 0.3s ease;
}
.buttons{
display:flex;gap:12px;margin-top:24px;
flex-wrap:wrap;
}
.btn{
appearance:none;border:0;border-radius:12px;
padding:12px 24px;font-weight:600;cursor:pointer;
font-size:14px;transition:all 0.2s ease;
display:flex;align-items:center;gap:8px;
text-decoration:none;justify-content:center;
min-width:120px;
}
.btn:disabled{
opacity:0.6;cursor:not-allowed;
}
.btn-primary{
background:linear-gradient(135deg,#667eea,#764ba2);
color:white;box-shadow:0 4px 12px rgba(102,126,234,0.4);
}
.btn-primary:hover:not(:disabled){
transform:translateY(-1px);
box-shadow:0 6px 16px rgba(102,126,234,0.5);
}
.btn-secondary{
background:#f3f4f6;color:#374151;
border:1px solid #d1d5db;
}
.btn-secondary:hover:not(:disabled){
background:#e5e7eb;transform:translateY(-1px);
}
.preview-container{
position:relative;
}
.preview{
white-space:pre-wrap;
font-family:'SF Mono',Monaco,'Cascadia Code','Roboto Mono',Consolas,'Courier New',monospace;
font-size:12px;line-height:1.6;
border:2px solid #e5e7eb;border-radius:12px;
padding:16px;background:#f9fafb;
max-height:70vh;overflow:auto;
transition:all 0.2s ease;
}
.preview:hover{
border-color:#d1d5db;
}
.error{
color:#dc2626;font-size:14px;margin-top:8px;
padding:8px 12px;background:#fef2f2;
border:1px solid #fecaca;border-radius:8px;
display:flex;align-items:center;gap:8px;
}
.success{
color:#059669;font-size:14px;margin-top:8px;
padding:8px 12px;background:#ecfdf5;
border:1px solid #a7f3d0;border-radius:8px;
display:flex;align-items:center;gap:8px;
}
.meta-info{
color:#6b7280;font-size:12px;margin-top:12px;
padding:8px 12px;background:#f3f4f6;
border-radius:8px;display:flex;gap:16px;
flex-wrap:wrap;
}
.meta-item{
display:flex;align-items:center;gap:4px;
}
.loading{
display:inline-block;width:16px;height:16px;
border:2px solid #e5e7eb;border-top:2px solid #667eea;
border-radius:50%;animation:spin 1s linear infinite;
}
@keyframes spin{
0%{transform:rotate(0deg)}
100%{transform:rotate(360deg)}
}
.fade-in{
animation:fadeIn 0.3s ease;
}
@keyframes fadeIn{
from{opacity:0;transform:translateY(10px)}
to{opacity:1;transform:translateY(0)}
}
.pulse{
animation:pulse 2s infinite;
}
@keyframes pulse{
0%,100%{opacity:1}
50%{opacity:0.7}
}
@media(max-width:768px){
.wrap{padding:16px}
.buttons{flex-direction:column}
.btn{width:100%}
.header h1{font-size:24px}
.card{padding:20px}
}
</style>
</head>
<body>
<div class="wrap">
<div class="header">
<h1>📋 Редактирование заявления</h1>
<p>Проверьте и при необходимости отредактируйте все поля</p>
</div>
<div class="statement-container fade-in">
<div id="statement" class="statement-text">Загрузка…</div>
<div class="buttons" style="margin-top:32px;justify-content:center">
<button id="confirmBtn" class="btn btn-primary">
✅ Подтвердить и отправить
</button>
</div>
<div id="status" style="margin-top:16px"></div>
</div>
</div>
<script id="case-data" type="application/json">${caseJson}</script>
<script>
(function(){
console.log('=== СКРИПТ ЗАПУЩЕН ===');
console.log('Document ready state:', document.readyState);
console.log('Current time:', new Date().toISOString());
// Получаем данные
function getData(){
try {
console.log('=== ПАРСИНГ ДАННЫХ ===');
var dataEl = document.getElementById('case-data');
console.log('case-data element found:', !!dataEl);
if (!dataEl) {
console.error('Элемент #case-data не найден!');
return {};
}
var textContent = dataEl.textContent || '{}';
console.log('Raw JSON length:', textContent.length);
console.log('Raw JSON preview:', textContent.substring(0, 200));
var parsed = JSON.parse(textContent);
console.log('Parsed data:', parsed);
return parsed;
} catch(e) {
console.error('ОШИБКА ПАРСИНГА JSON:', e);
console.error('Текст для парсинга:', dataEl ? dataEl.textContent : 'элемент не найден');
return {};
}
}
// Простая функция экранирования
function esc(s){
if (s === null || s === undefined) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
}
// Простые функции валидации
function isValidPhone(phone) {
if (!phone) return false;
var clean = phone.replace(/\D/g, '');
return clean.length >= 10 && clean.length <= 11;
}
function isValidEmail(email) {
if (!email) return false;
return email.includes('@') && email.includes('.') && email.length > 5;
}
function isNotEmpty(value) {
return value && value.trim().length > 0;
}
// Преобразования дат
function parseDDMMYYYY(s){ // "31.12.2024" -> Date | null
console.log('parseDDMMYYYY вызвана с:', s);
// ИСПРАВЛЕНИЕ: используем [0-9] вместо \d
if (!/^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$/.test(s)) {
console.log('parseDDMMYYYY: формат не подходит');
return null;
}
var [d,m,y] = s.split('.').map(Number);
console.log('parseDDMMYYYY: день=', d, 'месяц=', m, 'год=', y);
var dt = new Date(y, m-1, d);
var isValid = (dt.getFullYear()===y && dt.getMonth()===m-1 && dt.getDate()===d);
console.log('parseDDMMYYYY: результат=', dt, 'валидна=', isValid);
return isValid ? dt : null;
}
function parseYMD(s){ // "2024-12-31" -> Date | null
console.log('parseYMD вызвана с:', s);
console.log('parseYMD: длина строки:', s.length);
console.log('parseYMD: символы:', s.split('').map(c => c.charCodeAt(0)));
console.log('parseYMD: тест регексом:', /^\d{4}-\d{2}-\d{2}$/.test(s));
// Попробуем очистить строку от возможных невидимых символов
var cleanS = s.trim();
// ВРЕМЕННОЕ ИСПРАВЛЕНИЕ: заменяем любые дефисоподобные символы на обычные дефисы
cleanS = cleanS.replace(/[\u2012\u2013\u2014\u2015\u2212\uFF0D]/g, '-');
console.log('parseYMD: после замены дефисов:', cleanS);
console.log('parseYMD: очищенная строка:', cleanS, 'длина:', cleanS.length);
console.log('parseYMD: тест очищенной регексом:', /^\d{4}-\d{2}-\d{2}$/.test(cleanS));
// ДОПОЛНИТЕЛЬНАЯ ОТЛАДКА РЕГЕКСА
console.log('parseYMD: проверка по частям:');
console.log(' - первые 4 символа (год):', cleanS.substring(0,4), 'тест \\d{4}:', /^\d{4}$/.test(cleanS.substring(0,4)), 'тест [0-9]{4}:', /^[0-9]{4}$/.test(cleanS.substring(0,4)));
console.log(' - символ 4 (дефис):', cleanS.charAt(4), 'код:', cleanS.charCodeAt(4));
console.log(' - символы 5-6 (месяц):', cleanS.substring(5,7), 'тест \\d{2}:', /^\d{2}$/.test(cleanS.substring(5,7)), 'тест [0-9]{2}:', /^[0-9]{2}$/.test(cleanS.substring(5,7)));
console.log(' - символ 7 (дефис):', cleanS.charAt(7), 'код:', cleanS.charCodeAt(7));
console.log(' - символы 8-9 (день):', cleanS.substring(8,10), 'тест \\d{2}:', /^\d{2}$/.test(cleanS.substring(8,10)), 'тест [0-9]{2}:', /^[0-9]{2}$/.test(cleanS.substring(8,10)));
// ИСПРАВЛЕНИЕ: используем [0-9] вместо \d
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(cleanS)) {
console.log('parseYMD: формат не подходит');
return null;
}
s = cleanS; // используем очищенную строку
var [y,m,d] = s.split('-').map(Number);
console.log('parseYMD: год=', y, 'месяц=', m, 'день=', d);
var dt = new Date(y, m-1, d);
var isValid = (dt.getFullYear()===y && dt.getMonth()===m-1 && dt.getDate()===d);
console.log('parseYMD: результат=', dt, 'валидна=', isValid);
return isValid ? dt : null;
}
function toYMD(dt){ // Date -> "YYYY-MM-DD"
var y = dt.getFullYear();
var m = String(dt.getMonth()+1).padStart(2,'0');
var d = String(dt.getDate()).padStart(2,'0');
return y+'-'+m+'-'+d;
}
function clampDate(dt, minDt, maxDt){
if (minDt && dt < minDt) return false;
if (maxDt && dt > maxDt) return false;
return true;
}
// Глобальные референсы
var TODAY = new Date(); TODAY.setHours(0,0,0,0);
console.log('=== ОТЛАДКА ДАТ ===');
console.log('Текущая дата (TODAY):', TODAY.toISOString());
console.log('Текущая дата (локальная):', TODAY.toLocaleDateString('ru-RU'));
console.log('Год:', TODAY.getFullYear(), 'Месяц:', TODAY.getMonth()+1, 'День:', TODAY.getDate());
var MIN_1900 = new Date(1900,0,1);
var MIN_2000 = new Date(2000,0,1);
var PLUS_10Y = new Date(TODAY.getFullYear()+10, TODAY.getMonth(), TODAY.getDate());
// Единая проверка дат по ключу поля
function validateDateByKey(key, rawValue, field){
// rawValue может быть "YYYY-MM-DD" (input[type=date]) или "DD.MM.YYYY" (сохранённое)
console.log('validateDateByKey called:', {key, rawValue, type: typeof rawValue});
var dt = null;
if (typeof rawValue === 'string' && rawValue.includes('-')) dt = parseYMD(rawValue);
if (!dt && typeof rawValue === 'string' && rawValue.includes('.')) dt = parseDDMMYYYY(rawValue);
console.log('Parsed date:', {dt, today: TODAY});
if (!dt) return { ok:false, msg:'Некорректная дата' };
// Бизнес-правила по полям
if (key === 'birthday'){
// Дата рождения: 1900-01-01 … сегодня
if (dt > TODAY) return { ok:false, msg:'Дата рождения не может быть в будущем' };
if (dt < MIN_1900) return { ok:false, msg:'Дата рождения не может быть ранее 01.01.1900' };
return { ok:true };
}
if (key === 'agrdate'){
// Дата договора/события: 2000-01-01 … сегодня (без будущего)
if (!clampDate(dt, MIN_2000, TODAY)) return { ok:false, msg:'Дата договора не может быть в будущем' };
return { ok:true };
}
if (key === 'startdate' || key === 'finishdate'){
// Период: 2000-01-01 … сегодня+10 лет
if (!clampDate(dt, MIN_2000, PLUS_10Y)) return { ok:false, msg:'Дата вне допустимого диапазона' };
// Доп. правило: start <= finish
var root = field && field.getAttribute('data-root');
if (root === 'project'){
var s = (state.project && state.project.startdate) || '';
var f = (state.project && state.project.finishdate) || '';
var sd = s ? (s.includes('-') ? parseYMD(s) : parseDDMMYYYY(s)) : null;
var fd = f ? (f.includes('-') ? parseYMD(f) : parseDDMMYYYY(f)) : null;
if (key === 'startdate' && fd && dt > fd) return { ok:false, msg:'Дата начала не может быть позже даты окончания' };
if (key === 'finishdate' && sd && dt < sd) return { ok:false, msg:'Дата окончания не может быть раньше даты начала' };
}
return { ok:true };
}
// По умолчанию — только реальная дата
return { ok:true };
}
// Обновлённая простая проверка для внешних вызовов (сохраним совместимость)
function isValidDate(dateStr){
var dt = parseDDMMYYYY(dateStr);
if (!dt) return false;
// базовые разумные границы
var currentYear = TODAY.getFullYear();
var minYear = currentYear - 100;
var maxYear = currentYear + 50;
return dt.getFullYear() >= minYear && dt.getFullYear() <= maxYear;
}
// ✅ ПЕРЕНЕСЕНО из test/index.php
// Логика валидации ИНН на основе масок из тестового файла:
// - js-inn-mask: "999999999999" (строго 12 цифр для физлиц)
// - js-inn-mask2: "9{10,12}" (10-12 цифр для юрлиц)
function isValidINN(inn, isIndividual) {
if (!inn) return false;
var clean = String(inn).replace(/\D/g, '');
// Логика из test/index.php - проверка формата и длины
// Маска js-inn-mask: 12 цифр для физлиц
// Маска js-inn-mask2: 10-12 цифр для универсального использования
if (isIndividual) {
// Физлицо - строго 12 цифр (как в js-inn-mask)
return clean.length === 12;
} else {
// Юрлицо - 10 или 12 цифр (как в js-inn-mask2)
return clean.length === 10 || clean.length === 12;
}
}
function isValidName(name) {
if (!name) return false;
var trimmed = name.trim();
if (trimmed.length < 2) return false;
// Только кириллица, дефисы, апострофы, пробелы
// Проверяем по частям чтобы избежать проблем с регулярками
var hasCyrillic = /[а-яёА-ЯЁ]/.test(trimmed);
var hasInvalidChars = /[a-zA-Z0-9]/.test(trimmed); // латиница или цифры
var hasOnlyAllowed = true; // Временно отключаем сложную проверку
return hasCyrillic && !hasInvalidChars && hasOnlyAllowed;
}
function isValidAddress(address) {
if (!address) return false;
var trimmed = address.trim();
if (trimmed.length < 10) return false;
// Адрес должен содержать цифры (номер дома) и буквы
// Используем [0-9] для надёжного поиска цифр (исправлена проблема с /\d/)
var hasNumbers = /[0-9]/.test(trimmed);
var cyrillicPattern = new RegExp('[а-яёА-ЯЁ]');
var hasLetters = cyrillicPattern.test(trimmed);
return hasNumbers && hasLetters;
}
function isValidMoney(v) {
if (!v) return false;
var s = String(v).replace(/\s+/g,'').replace(',', '.');
if (!/^[0-9]+(\.[0-9]{1,2})?$/.test(s)) return false; // ИСПРАВИЛ: заменил \d на [0-9]
return parseFloat(s) > 0;
}
// Красивая валидация поля
function updateFieldStyle(field) {
// --- корректное чтение значения ---
var key = field.getAttribute('data-key') || '';
var value;
if (field.type === 'checkbox') {
value = field.checked; // ✅ вместо field.value
} else {
value = field.value || '';
}
// Сброс классов
field.classList.remove('required-empty', 'valid', 'verified');
var isValid = true;
var isEmpty = (field.type === 'checkbox') ? !value : !value || value.trim() === '';
// Централизованный список обязательных полей
var REQUIRED = new Set(['lastname','firstname','birthday','birthplace','mailingstreet','inn','agrdate','agrprice','accountname','address','reason','description']);
var isRequired = REQUIRED.has(key);
if (key === 'mobile') {
isValid = isEmpty || isValidPhone(value);
} else if (key === 'email') {
isValid = isEmpty || isValidEmail(value);
} else if (key === 'inn') {
// Определяем тип ИНН по контексту
var root = field.getAttribute('data-root');
var isIndividual = (root === 'user'); // user = физлицо, offender = юрлицо
if (isEmpty) {
isValid = !isRequired; // Если обязательное - невалидно, если нет - валидно
} else {
isValid = isValidINN(value, isIndividual);
}
} else if (key === 'birthday' || key === 'agrdate' || key === 'startdate' || key === 'finishdate') {
if (isEmpty) {
isValid = !isRequired;
} else {
console.log('Date validation debug:', {
key: key,
value: value,
fieldType: field.type,
fieldValue: field.value,
isEmpty: isEmpty
});
var dateValidation = validateDateByKey(key, value, field);
isValid = dateValidation.ok;
console.log('Date validation result:', dateValidation);
// Сохраняем сообщение об ошибке для вывода
if (!isValid) {
field.setAttribute('data-error-message', dateValidation.msg);
} else {
field.removeAttribute('data-error-message');
}
}
} else if (['lastname', 'firstname', 'secondname'].includes(key)) {
isValid = isEmpty || isValidName(value);
if (isRequired && isEmpty) isValid = false; // ФИО обязательно
} else if (key === 'birthplace') {
isValid = isEmpty || isValidName(value);
if (isRequired && isEmpty) isValid = false; // Место рождения обязательно
} else if (key === 'mailingstreet' || key === 'address') {
isValid = isEmpty || isValidAddress(value);
if (isRequired && isEmpty) isValid = false; // Адрес обязателен
} else if (key === 'agrprice') {
isValid = isEmpty ? !isRequired : isValidMoney(value);
if (isRequired && isEmpty) isValid = false;
} else if (['accountname', 'subject', 'reason', 'description'].includes(key)) {
isValid = isEmpty || isNotEmpty(value);
if (isRequired && isEmpty) isValid = false; // Эти поля обязательны
} else if (key === 'privacyConsent') {
// Для чекбокса согласия - должен быть отмечен
isValid = value === true;
}
// --- вывод сообщения только для ошибок ---
var msgEl = document.getElementById('msg_' + field.id);
if (msgEl) {
if (!isEmpty && !isValid) {
msgEl.style.display = 'block';
msgEl.className = 'validation-message error';
// Человечные сообщения об ошибках
var errorMessage = '';
if (key === 'inn') {
// Логика из test/index.php - разные требования для физлиц и юрлиц
var root = field.getAttribute('data-root');
var isIndividual = (root === 'user');
errorMessage = isIndividual
? 'ИНН физлица: ровно 12 цифр (как js-inn-mask)'
: 'ИНН организации: 10 или 12 цифр (как js-inn-mask2)';
} else if (key === 'agrprice') {
errorMessage = 'Введите сумму числом (например: 50000 или 50000.50)';
} else if (['lastname', 'firstname', 'secondname', 'birthplace'].includes(key)) {
errorMessage = 'Только русские буквы, без цифр и латиницы';
} else if (key === 'mailingstreet' || key === 'address') {
errorMessage = 'Укажите полный адрес с номером дома';
} else if (key === 'email') {
errorMessage = 'Неверный формат email';
} else if (key === 'mobile') {
errorMessage = 'Неверный формат телефона';
} else if (key.includes('date')) {
errorMessage = field.getAttribute('data-error-message') || 'Выберите корректную дату';
} else {
errorMessage = 'Поле заполнено неверно';
}
msgEl.textContent = errorMessage;
} else {
msgEl.style.display = 'none';
msgEl.textContent = '';
}
}
// Добавляем соответствующий класс с анимацией
if (!isEmpty && isValid) {
field.classList.add('valid');
} else if (!isEmpty && !isValid) {
field.classList.add('required-empty');
}
}
// Шаблоны текстов для разных типов услуг
var serviceTemplates = {
education: {
name: 'Образовательные услуги',
contractText: ' г. мною был заключен договор с {company} об оказании платных образовательных услуг{subject}.',
paymentText: 'По указанному договору мною внесена оплата в размере {amount} рублей.',
reasonText: 'В настоящий момент, в связи с неудовлетворительным качеством услуги, мною принято решение о расторжении договора и возврате уплаченных по договору денежных средств.',
placeholders: {
subject: ' по программе {subject}'
}
},
tourism: {
name: 'Туристические услуги',
contractText: ' г. мною был заключен договор с {company} об оказании туристических услуг{subject}.',
paymentText: 'По указанному договору мною внесена предоплата в размере {amount} рублей.',
reasonText: 'В настоящий момент, в связи с некачественным оказанием услуг/невозможностью оказания услуг, мною принято решение о расторжении договора и возврате уплаченных денежных средств.',
placeholders: {
subject: ' ({subject})'
}
},
medical: {
name: 'Медицинские услуги',
contractText: ' г. мною был заключен договор с {company} об оказании платных медицинских услуг{subject}.',
paymentText: 'По указанному договору мною внесена оплата в размере {amount} рублей.',
reasonText: 'В настоящий момент, в связи с неудовлетворительным качеством оказанных медицинских услуг, мною принято решение о расторжении договора и возврате уплаченных денежных средств.',
placeholders: {
subject: ' ({subject})'
}
},
fitness: {
name: 'Фитнес/спортивные услуги',
contractText: ' г. мною был заключен договор с {company} об оказании услуг фитнес-клуба/спортивных услуг{subject}.',
paymentText: 'По указанному договору мною внесена оплата в размере {amount} рублей.',
reasonText: 'В настоящий момент, в связи с неудовлетворительным качеством услуг/невозможностью посещения, мною принято решение о расторжении договора и возврате уплаченных денежных средств.',
placeholders: {
subject: ' ({subject})'
}
},
other: {
name: 'Другие услуги',
contractText: ' г. мною был заключен договор с {company} об оказании {serviceType}{subject}.',
paymentText: 'По указанному договору мною внесена оплата в размере {amount} рублей.',
reasonText: 'В настоящий момент, в связи с {reason}, мною принято решение о расторжении договора и возврате уплаченных по договору денежных средств.',
placeholders: {
subject: ' ({subject})',
serviceType: 'услуг',
reason: 'неудовлетворительным качеством услуги'
}
}
};
// Получаем данные
var injected = getData();
console.log('Data loaded:', injected);
var state = (injected && injected.case) ? injected.case : {
user: {},
project: {},
offenders: [{}],
meta: {}
};
// Добавляем поля для шаблонов, если их нет
if (!state.project.serviceType) state.project.serviceType = 'education';
if (!state.project.customServiceType) state.project.customServiceType = '';
if (!state.project.customReason) state.project.customReason = '';
// Добавляем поле согласия на обработку персданных
if (state.meta.privacyConsent === undefined) state.meta.privacyConsent = false;
// Добавляем поля для SMS подтверждения
if (state.meta.smsCodeSent === undefined) state.meta.smsCodeSent = false;
if (state.meta.smsCode === undefined) state.meta.smsCode = '';
if (state.meta.smsVerified === undefined) state.meta.smsVerified = false;
console.log('State:', state);
console.log('SMS meta fields:', {
session_token: state.meta.session_token,
prefix: state.meta.prefix,
telegram_id: state.meta.telegram_id,
claim_id: state.meta.claim_id,
unified_id: state.meta.unified_id,
user_id: state.meta.user_id
});
// Красивая функция для создания поля
function createField(root, key, value, placeholder, index) {
var id = 'field_' + root + '_' + key + '_' + (index !== undefined ? index + '_' : '') + Math.random().toString(36).slice(2);
var dataIndex = index !== undefined ? ' data-index="' + index + '"' : '';
// атрибуты по умолчанию
var extra = '';
// 🔒 для ИНН — только цифры, логика из test/index.php
if (key === 'inn') {
// Определяем тип по контексту (физлицо vs юрлицо)
var isIndividual = (root === 'user'); // user = физлицо, offender = юрлицо
if (isIndividual) {
// Физлицо: строго 12 цифр (как js-inn-mask: "999999999999")
extra = ' inputmode="numeric" pattern="\\d{12}" maxlength="12" autocomplete="off" title="ИНН физического лица: ровно 12 цифр"';
if (!placeholder) placeholder = 'ИНН физлица (12 цифр)';
} else {
// Юрлицо: 10-12 цифр (как js-inn-mask2: "9{10,12}")
extra = ' inputmode="numeric" pattern="\\d{10,12}" maxlength="12" autocomplete="off" title="ИНН организации: 10 или 12 цифр"';
if (!placeholder) placeholder = 'ИНН организации (10-12 цифр)';
}
}
var fieldHtml =
'<input class="inline-field bind" data-root="' + esc(root) + '" data-key="' + esc(key) + '"' + dataIndex +
' id="' + id + '" value="' + esc(value || '') + '" placeholder="' + esc(placeholder || '') + '"' + extra + ' />';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return fieldHtml + msgHtml;
}
// Функция для создания readonly поля
function createReadonlyField(root, key, value, label) {
var id = 'field_' + root + '_' + key + '_readonly_' + Math.random().toString(36).slice(2);
var fieldHtml = '<input class="inline-field readonly-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" value="' + esc(value || '') + '" readonly />';
return fieldHtml;
}
// Функция для создания поля суммы с суффиксом "рублей" и автозаменой запятой
function createMoneyField(root, key, value, placeholder, index) {
var id = 'field_' + root + '_' + key + '_' + (index !== undefined ? index + '_' : '') + Math.random().toString(36).slice(2);
var dataIndex = index !== undefined ? ' data-index="' + index + '"' : '';
// Поле ввода с автозаменой запятой на точку
var fieldHtml =
'<div style="display: flex; align-items: center; gap: 8px;">' +
'<input class="inline-field bind money-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '"' + dataIndex +
' id="' + id + '" value="' + esc(value || '') + '" placeholder="' + esc(placeholder || '') + '"' +
' inputmode="decimal" pattern="[0-9]*[.,]?[0-9]*" autocomplete="off" title="Введите сумму (можно использовать запятую или точку)" />' +
'<span style="color: #6b7280; font-size: 14px;">рублей</span>' +
'</div>';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return fieldHtml + msgHtml;
}
// Функция для создания чекбокса
function createCheckbox(root, key, checked, labelText, required) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
var checkedAttr = checked ? ' checked' : '';
var requiredClass = required ? ' required-checkbox' : '';
var checkboxHtml = '<label class="checkbox-container' + requiredClass + '" for="' + id + '">';
checkboxHtml += '<input type="checkbox" class="checkbox-field bind" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '"' + checkedAttr + ' />';
checkboxHtml += '<span class="checkmark"></span>';
checkboxHtml += '<span class="checkbox-label">' + labelText + '</span>';
checkboxHtml += '</label>';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return checkboxHtml + msgHtml;
}
// Функция для создания textarea
function createTextarea(root, key, value, placeholder) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
var fieldHtml = '<textarea class="inline-field bind full-width" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" placeholder="' + esc(placeholder || '') + '">' + esc(value || '') + '</textarea>';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return fieldHtml + msgHtml;
}
// Функция для создания поля даты с календариком
function createDateField(root, key, value, placeholder) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
console.log('createDateField called:', 'root=' + root, 'key=' + key, 'value=' + value, 'type=' + typeof value);
// Конвертируем дату из различных форматов в YYYY-MM-DD для input[type="date"]
var dateValue = '';
if (value) {
var cleanValue = String(value).trim();
if (cleanValue.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)) {
// Уже в формате YYYY-MM-DD
dateValue = cleanValue;
} else if (cleanValue.match(/^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$/)) {
// Формат DD.MM.YYYY -> YYYY-MM-DD
var parts = cleanValue.split('.');
dateValue = parts[2] + '-' + parts[1] + '-' + parts[0];
}
}
console.log('createDateField result:', 'originalValue=' + value, 'convertedValue=' + dateValue, 'willCreateHTML=' + (dateValue ? 'YES' : 'NO'));
var fieldHtml = '<input type="date" class="inline-field bind date-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" value="' + esc(dateValue) + '" placeholder="' + esc(placeholder || '') + '" />';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return fieldHtml + msgHtml;
}
// Функция для создания выпадающего списка
function createSelectField(root, key, value, options, placeholder) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
var fieldHtml = '<select class="inline-field bind select-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '">';
if (placeholder) {
fieldHtml += '<option value="">' + esc(placeholder) + '</option>';
}
for (var optKey in options) {
var selected = value === optKey ? ' selected' : '';
fieldHtml += '<option value="' + esc(optKey) + '"' + selected + '>' + esc(options[optKey]) + '</option>';
}
fieldHtml += '</select>';
var msgId = 'msg_' + id;
var msgHtml = '<div id="' + msgId + '" class="validation-message" style="display:none"></div>';
return fieldHtml + msgHtml;
}
// Функция для обработки шаблонов текста
function processTemplate(template, data) {
var result = template;
// Заменяем основные плейсхолдеры
result = result.replace(/\{company\}/g, data.company || '');
result = result.replace(/\{amount\}/g, data.amount || '');
// Обрабатываем условные блоки для subject
if (data.subject && data.subject.trim() !== '') {
var subjectTemplate = data.subjectTemplate || ' ({subject})';
var subjectText = subjectTemplate.replace(/\{subject\}/g, data.subject);
result = result.replace(/\{subject\}/g, subjectText);
} else {
result = result.replace(/\{subject\}/g, '');
}
// Обрабатываем дополнительные плейсхолдеры для типа "other"
result = result.replace(/\{serviceType\}/g, data.serviceType || 'услуг');
result = result.replace(/\{reason\}/g, data.reason || 'неудовлетворительным качеством услуги');
return result;
}
// Функция проверки всех обязательных полей
function validateAllFields() {
var requiredFields = [
{ root: 'user', key: 'lastname', name: 'Фамилия' },
{ root: 'user', key: 'firstname', name: 'Имя' },
{ root: 'user', key: 'birthday', name: 'Дата рождения' },
{ root: 'user', key: 'birthplace', name: 'Место рождения' },
{ root: 'user', key: 'mailingstreet', name: 'Адрес' },
{ root: 'user', key: 'inn', name: 'ИНН' },
{ root: 'project', key: 'agrdate', name: 'Дата договора' },
{ root: 'project', key: 'agrprice', name: 'Сумма' },
{ root: 'project', key: 'reason', name: 'Причина обращения' },
{ root: 'project', key: 'description', name: 'Описание проблемы' },
{ root: 'offender', key: 'accountname', name: 'Название организации' },
{ root: 'offender', key: 'address', name: 'Адрес организации' }
];
var errors = [];
for (var i = 0; i < requiredFields.length; i++) {
var field = requiredFields[i];
var value = '';
if (field.root === 'user') {
value = state.user[field.key] || '';
} else if (field.root === 'project') {
value = state.project[field.key] || '';
} else if (field.root === 'offender') {
value = (state.offenders[0] && state.offenders[0][field.key]) || '';
}
if (!value || (typeof value === 'string' && value.trim() === '')) {
errors.push(field.name);
}
}
return errors;
}
// Функция обновления состояния кнопки отправки
function updateSubmitButton() {
var confirmBtn = document.getElementById('confirmBtn');
if (!confirmBtn) return;
var isConsentGiven = state.meta && state.meta.privacyConsent === true;
var smsCodeSent = state.meta && state.meta.smsCodeSent === true;
var smsVerified = state.meta && state.meta.smsVerified === true;
if (!isConsentGiven) {
// Нет согласия - кнопка заблокирована
confirmBtn.disabled = true;
confirmBtn.style.opacity = '0.6';
confirmBtn.style.cursor = 'not-allowed';
confirmBtn.textContent = '✅ Подтвердить и отправить';
} else if (!smsCodeSent) {
// Согласие есть, SMS не отправлена - проверяем валидность основных полей
var validationErrors = validateAllFields();
if (validationErrors.length === 0) {
confirmBtn.disabled = false;
confirmBtn.style.opacity = '1';
confirmBtn.style.cursor = 'pointer';
confirmBtn.textContent = '📱 Получить SMS код';
} else {
confirmBtn.disabled = true;
confirmBtn.style.opacity = '0.6';
confirmBtn.style.cursor = 'not-allowed';
confirmBtn.textContent = '❌ Заполните все поля (' + validationErrors.length + ')';
confirmBtn.title = 'Не заполнены: ' + validationErrors.join(', ');
}
} else if (smsCodeSent && !smsVerified) {
// SMS отправлена, но не подтверждена - кнопка заблокирована
confirmBtn.disabled = true;
confirmBtn.style.opacity = '0.6';
confirmBtn.style.cursor = 'not-allowed';
confirmBtn.textContent = '⏳ Введите код из SMS';
} else if (smsVerified) {
// SMS подтверждена - проверяем валидность всех полей
var validationErrors = validateAllFields();
if (validationErrors.length === 0) {
confirmBtn.disabled = false;
confirmBtn.style.opacity = '1';
confirmBtn.style.cursor = 'pointer';
confirmBtn.textContent = '🚀 Отправить заявление';
} else {
confirmBtn.disabled = true;
confirmBtn.style.opacity = '0.6';
confirmBtn.style.cursor = 'not-allowed';
confirmBtn.textContent = '❌ Заполните все поля (' + validationErrors.length + ')';
confirmBtn.title = 'Не заполнены: ' + validationErrors.join(', ');
}
}
}
// Рендерим заявление
function renderStatement() {
console.log('=== НАЧАЛО РЕНДЕРИНГА ===');
console.log('state:', state);
var u = state.user || {};
var o = (state.offenders && state.offenders[0]) || {};
var p = state.project || {};
console.log('user data:', u);
console.log('offender data:', o);
console.log('project data:', p);
console.log('=== ОТЛАДКА ДАТ ПРОЕКТА ===');
console.log('p.agrdate (дата договора):', p.agrdate, 'тип:', typeof p.agrdate);
console.log('p.startdate (начало периода):', p.startdate, 'тип:', typeof p.startdate);
console.log('p.finishdate (конец периода):', p.finishdate, 'тип:', typeof p.finishdate);
var html = '';
console.log('Starting HTML generation...');
// Заголовок
html += '<div style="text-align:center;margin-bottom:32px">';
html += '<h2 style="font-size:20px;margin:0 0 16px;color:#1f2937">В МОО «Клиентправ»</h2>';
html += '<p style="margin:0;color:#6b7280">help@clientright.ru</p>';
html += '</div>';
// Данные заявителя
html += '<p><strong>Заявитель:</strong> ';
html += createField('user', 'lastname', u.lastname, 'Фамилия (обязательно)');
html += ' ';
html += createField('user', 'firstname', u.firstname, 'Имя (обязательно)');
html += ' ';
html += createField('user', 'secondname', u.secondname, 'Отчество');
html += '</p>';
html += '<p><strong>Дата рождения:</strong> ';
html += createDateField('user', 'birthday', u.birthday, 'дд.мм.гггг');
html += '</p>';
html += '<p><strong>Место рождения:</strong> ';
html += createField('user', 'birthplace', u.birthplace, 'Место рождения (обязательно)');
html += '</p>';
html += '<p><strong>ИНН:</strong> ';
html += createField('user', 'inn', u.inn, '12-значный ИНН (обязательно)');
html += '</p>';
html += '<p><strong>Адрес:</strong> ';
html += createField('user', 'mailingstreet', u.mailingstreet, 'Адрес регистрации как в паспорте (обязательно)');
html += '</p>';
html += '<p><strong>Телефон:</strong> ';
html += createReadonlyField('user', 'mobile', u.mobile, 'Телефон');
html += '</p>';
html += '<p><strong>E-mail:</strong> ';
html += createField('user', 'email', u.email, 'email@example.com');
html += '</p>';
html += '<div class="section-break"></div>';
// Возмещение
html += '<h3 style="font-size:16px;margin:0 0 16px;color:#1f2937">Возмещение:</h3>';
html += '<p>Выплата возмещения возможна по системе быстрых платежей (СБП) по номеру телефона заявителя: <strong id="phone-display">' + esc(u.mobile || '') + '</strong></p>';
html += '<div class="section-break"></div>';
// Заявление
html += '<h3 style="font-size:16px;margin:0 0 16px;color:#1f2937;text-align:center">ЗАЯВЛЕНИЕ</h3>';
// Тема обращения (только для чтения)
html += '<p><strong>Тема обращения:</strong> ';
html += createReadonlyField('project', 'category', p.category, 'Тема обращения');
html += '</p>';
// Название договора / предмет
html += '<p><strong>Предмет договора:</strong> ';
html += createField('project', 'subject', p.subject, 'Название договора или предмет услуг');
html += '</p>';
// Дата события / заключения договора
html += '<p><strong>Дата события / заключения договора:</strong> ';
html += createDateField('project', 'agrdate', p.agrdate, 'дд.мм.гггг');
html += '</p>';
// Сумма оплаты / стоимость
html += '<p><strong>Сумма оплаты / стоимость:</strong> ';
html += createMoneyField('project', 'agrprice', p.agrprice, 'Введите сумму');
html += '</p>';
// Период
html += '<p><strong>Период:</strong> ';
if (p.startdate || p.finishdate) {
html += 'с ';
html += createDateField('project', 'startdate', p.startdate, 'дд.мм.гггг');
html += ' по ';
html += createDateField('project', 'finishdate', p.finishdate, 'дд.мм.гггг');
} else {
html += createField('project', 'period_text', p.period_text, 'Период действия');
}
html += '</p>';
html += '<div class="section-break"></div>';
// Контрагенты / участники
html += '<h4 style="font-size:14px;margin:16px 0 12px;color:#1f2937">Контрагенты / участники:</h4>';
for (var i = 0; i < state.offenders.length; i++) {
var offender = state.offenders[i];
html += '<div style="margin-bottom:16px;padding:12px;border:1px solid #e5e7eb;border-radius:8px;">';
html += '<p><strong>Наименование:</strong> ';
html += createField('offender', 'accountname', offender.accountname, 'Название организации', i);
html += '</p>';
html += '<p><strong>ИНН:</strong> ';
html += createField('offender', 'inn', offender.inn, 'ИНН организации (10 или 12 цифр)', i);
html += '</p>';
html += '<p><strong>ОГРН:</strong> ';
html += createField('offender', 'ogrn', offender.ogrn, 'ОГРН', i);
html += '</p>';
html += '<p><strong>Адрес:</strong> ';
html += createField('offender', 'address', offender.address, 'Адрес', i);
html += '</p>';
html += '<p><strong>E-mail:</strong> ';
html += createField('offender', 'email', offender.email, 'email@example.com', i);
html += '</p>';
html += '<p><strong>Телефон:</strong> ';
html += createField('offender', 'phone', offender.phone, '+7 (___) ___-__-__', i);
html += '</p>';
html += '<p><strong>Сайт:</strong> ';
html += createField('offender', 'website', offender.website, 'https://example.com', i);
html += '</p>';
html += '</div>';
}
html += '<div class="section-break"></div>';
// Причина обращения (редактируемая)
html += '<p><strong>Причина обращения:</strong> ';
html += createField('project', 'reason', p.reason, 'Можете уточнить или изменить причину обращения');
html += '</p>';
html += '<p><strong>Описание проблемы:</strong></p>';
html += createTextarea('project', 'description', p.description, 'Подробное описание проблемы, обстоятельств, фактов, переписки, попыток урегулировать спор');
html += '<p>На основании вышеизложенного и руководствуясь ст. 45 Закона «О защите прав потребителей», ст. 3, ч. 1 ст. 46 ГПК РФ, прошу вас защитить мои потребительские права, обратиться в суд с заявлением о защите моих потребительских прав и/или с коллективным иском о защите группы потребителей, и представлять мои интересы во всех судебных органах РФ, а также обращаться с заявлениями во все госорганы, подавать претензии, письма и жалобы.</p>';
html += '<div class="section-break"></div>';
// Согласие на обработку персональных данных
html += '<div style="margin:24px 0;">';
html += createCheckbox('meta', 'privacyConsent', state.meta.privacyConsent,
'Я ознакомлен(а) и согласен(а) с <a href="https://clientright.ru/person" target="_blank">Политикой обработки персональных данных</a> и даю согласие на обработку моих персональных данных в соответствии с Федеральным законом от 27.07.2006 №152-ФЗ «О персональных данных»',
true);
html += '</div>';
// SMS подтверждение (показывается только после отправки кода)
if (state.meta.smsCodeSent) {
html += '<div class="sms-verification-container fade-in" style="margin:24px 0;padding:20px;border:2px solid #667eea;border-radius:12px;background:rgba(102,126,234,0.05);">';
html += '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">';
html += '<div style="width:24px;height:24px;background:#667eea;border-radius:50%;display:flex;align-items:center;justify-content:center;">';
html += '<span style="color:white;font-size:14px;">📱</span>';
html += '</div>';
html += '<div>';
html += '<h4 style="margin:0;color:#1f2937;font-size:16px;">Подтверждение по SMS</h4>';
html += '<p style="margin:4px 0 0;color:#6b7280;font-size:14px;">Код отправлен на номер ' + esc(state.user.mobile || '') + '</p>';
html += '</div>';
html += '</div>';
html += '<div style="display:flex;gap:12px;align-items:center;">';
html += '<input type="text" ';
html += 'inputmode="numeric" pattern="[0-9]*" ';
html += 'oninput="this.value=this.value.replace(/[^0-9]/g,\\"\\").slice(0,6)" ';
html += 'class="sms-code-input bind" data-root="meta" data-key="smsCode" ';
html += 'placeholder="Введите 6-значный код" maxlength="6" ';
html += 'style="width:180px;padding:12px;font-size:18px;text-align:center;letter-spacing:6px;border:2px solid #e5e7eb;border-radius:8px;" ';
html += 'value="' + esc(state.meta.smsCode || '') + '" />';
if (!state.meta.smsVerified) {
html += '<button type="button" class="btn btn-secondary" onclick="resendSMS()" style="padding:12px 16px;">';
html += 'Отправить повторно';
html += '</button>';
} else {
html += '<span style="color:#10b981;font-weight:600;">✅ Подтверждено</span>';
}
html += '</div>';
if (!state.meta.smsVerified) {
html += '<p style="margin:12px 0 0;color:#6b7280;font-size:13px;">';
html += 'Не получили код? Проверьте папку "Спам" или нажмите "Отправить повторно"';
html += '</p>';
}
html += '</div>';
}
// Список приложенных документов
if (state.attachments && state.attachments.length > 0) {
html += '<div class="section-break"></div>';
html += '<h4 style="font-size:14px;margin:16px 0 12px;color:#1f2937">Приложенные документы:</h4>';
html += '<div style="background:#f9fafb;padding:12px;border-radius:8px;border:1px solid #e5e7eb;">';
for (var i = 0; i < state.attachments.length; i++) {
var fileName = state.attachments[i];
html += '<div style="display:flex;align-items:center;margin-bottom:8px;padding:8px;background:white;border-radius:6px;border:1px solid #e5e7eb;">';
html += '<span style="color:#3b82f6;margin-right:8px;">📎</span>';
html += '<span style="color:#374151;font-size:14px;">' + esc(fileName) + '</span>';
html += '</div>';
}
html += '<p style="margin:8px 0 0;color:#6b7280;font-size:12px;">';
html += 'Всего документов: ' + state.attachments.length;
html += '</p>';
html += '</div>';
}
// Вставляем HTML
console.log('Generated HTML length:', html.length);
console.log('HTML preview (first 200 chars):', html.substring(0, 200));
var statementEl = document.getElementById('statement');
if (statementEl) {
console.log('Setting innerHTML...');
statementEl.innerHTML = html;
console.log('innerHTML set successfully');
console.log('Calling attachBindings...');
attachBindings();
// Обновляем состояние кнопки отправки
setTimeout(function() {
updateSubmitButton();
}, 100);
console.log('=== РЕНДЕРИНГ ЗАВЕРШЕН УСПЕШНО ===');
} else {
console.error('ОШИБКА: элемент #statement не найден в renderStatement!');
}
}
// Красивые обработчики событий
function attachBindings() {
console.log('Attaching bindings...');
var fields = document.querySelectorAll('.bind');
console.log('Found fields:', fields.length);
Array.prototype.forEach.call(fields, function(field) {
// Обработка ввода
field.addEventListener('input', function() {
// Автозамена запятой на точку для денежных полей
if (this.classList.contains('money-field') && this.value.includes(',')) {
this.value = this.value.replace(/,/g, '.');
console.log('Заменена запятая на точку:', this.value);
}
var root = this.getAttribute('data-root');
var key = this.getAttribute('data-key');
var value = this.type === 'checkbox' ? this.checked : this.value;
// Для полей дат конвертируем YYYY-MM-DD в DD.MM.YYYY для сохранения
if (this.classList.contains('date-field') && value) {
var parts = value.split('-');
if (parts.length === 3) {
value = parts[2] + '.' + parts[1] + '.' + parts[0];
}
}
console.log('Field changed:', root, key, value);
// Обновляем состояние
if (root === 'user') {
state.user = state.user || {};
state.user[key] = value;
// Обновляем телефон в СБП (хотя поле теперь readonly, оставляем на всякий случай)
if (key === 'mobile') {
var phoneDisplay = document.getElementById('phone-display');
if (phoneDisplay) {
phoneDisplay.textContent = value;
}
}
} else if (root === 'project') {
state.project = state.project || {};
state.project[key] = value;
// При смене типа услуги перерендериваем форму
if (key === 'serviceType') {
console.log('Service type changed to:', value);
setTimeout(function() {
renderStatement();
}, 50);
return; // Не выполняем валидацию, так как элемент будет пересоздан
}
} else if (root === 'offender') {
if (!Array.isArray(state.offenders)) state.offenders = [];
var index = parseInt(this.getAttribute('data-index') || '0', 10);
state.offenders[index] = state.offenders[index] || {};
state.offenders[index][key] = value;
} else if (root === 'meta') {
state.meta = state.meta || {};
state.meta[key] = value;
// Для чекбокса согласия обновляем состояние кнопки
if (key === 'privacyConsent') {
updateSubmitButton();
}
// Для SMS кода автоматически проверяем при вводе 6 цифр
if (key === 'smsCode' && value.length === 6) {
console.log('SMS code entered, verifying...');
verifySMS(value);
}
}
// Валидация на лету
updateFieldStyle(field);
// Обновляем состояние кнопки при изменении любого поля
updateSubmitButton();
});
// ✅ ПЕРЕНЕСЕНО из test/index.php - блокировка нечисловых символов для ИНН
field.addEventListener('keydown', function(e) {
var key = this.getAttribute('data-key');
if (key === 'inn') {
// Разрешаем только цифры и служебные клавиши
var isDigit = (e.key >= '0' && e.key <= '9');
var isServiceKey = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key);
var isCtrlKey = e.ctrlKey && ['a', 'c', 'v', 'x', 'z'].includes(e.key.toLowerCase());
// Блокируем всё, кроме цифр и служебных клавиш
if (!isDigit && !isServiceKey && !isCtrlKey) {
e.preventDefault();
console.log('Blocked non-numeric input for INN:', e.key);
}
}
});
// ✅ ПЕРЕНЕСЕНО из test/index.php - фильтрация при вставке для ИНН
field.addEventListener('paste', function(e) {
var key = this.getAttribute('data-key');
if (key === 'inn') {
e.preventDefault();
// Получаем вставляемый текст
var paste = (e.clipboardData || window.clipboardData).getData('text');
// Оставляем только цифры
var cleanPaste = paste.replace(/\D/g, '');
// Определяем максимальную длину в зависимости от типа
var root = this.getAttribute('data-root');
var isIndividual = (root === 'user');
var maxLength = 12; // И для физлиц, и для юрлиц максимум 12
// Обрезаем до нужной длины
cleanPaste = cleanPaste.slice(0, maxLength);
// Вставляем очищенный текст
this.value = cleanPaste;
// Запускаем событие input для обновления состояния
var inputEvent = new Event('input', { bubbles: true });
this.dispatchEvent(inputEvent);
console.log('Filtered paste for INN:', paste, '→', cleanPaste);
}
});
// Временно отключаем валидацию телефона для отладки
/*
field.addEventListener('keypress', function(e) {
var key = this.getAttribute('data-key');
if (key === 'mobile') {
// Разрешаем только цифры, пробелы, +, -, (, )
var allowedChars = /^[0-9\+\-\(\)\s]$/;
if (!allowedChars.test(e.key) && e.key !== 'Backspace' && e.key !== 'Delete' && e.key !== 'Tab' && e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') {
e.preventDefault();
}
}
});
*/
// Валидация при потере фокуса
field.addEventListener('blur', function() {
updateFieldStyle(this);
updateSubmitButton();
});
// Валидация при клике (для уже заполненных полей)
field.addEventListener('click', function() {
updateFieldStyle(this);
});
// Дополнительная обработка для чекбоксов
if (field.type === 'checkbox') {
field.addEventListener('change', function() {
var root = this.getAttribute('data-root');
var key = this.getAttribute('data-key');
var value = this.checked;
if (root === 'meta' && key === 'privacyConsent') {
state.meta = state.meta || {};
state.meta[key] = value;
updateSubmitButton();
// Валидация чекбокса
var container = this.closest('.checkbox-container');
if (container) {
if (value) {
container.classList.remove('error');
} else {
container.classList.add('error');
}
}
}
});
}
});
}
// Функции для работы с SMS
function sendSMS() {
console.log('=== ОТПРАВКА SMS ===');
console.log('Phone:', state.user.mobile);
console.log('SMS meta data:', state.meta);
console.log('Injected data:', injected);
console.log('Available fields:', {
'state.meta.claim_id': state.meta.claim_id,
'injected.claim_id': injected.claim_id,
'state.meta.prefix': state.meta.prefix,
'injected.prefix': injected.prefix,
'state.meta.telegram_id': state.meta.telegram_id,
'injected.telegram_id': injected.telegram_id
});
// Проверяем наличие всех необходимых данных
if (!state.user.mobile) {
console.error('ERROR: No phone number');
alert('Ошибка: не указан номер телефона');
return;
}
if (!state.meta.unified_id) {
console.error('ERROR: No unified_id');
alert('Ошибка: отсутствует unified_id');
return;
}
var sessionToken = state.meta.session_token || injected.session_token || '';
if (!sessionToken) {
console.error('ERROR: No session_token');
console.log('Checked state.meta.session_token:', state.meta.session_token);
console.log('Checked injected.session_token:', injected.session_token);
alert('Ошибка: отсутствует session_token');
return;
}
var smsData = injected.sms_meta || {};
var payload = {
message: {
phone_number: state.user.mobile,
claim_id: smsData.claim_id || '',
prefix: smsData.prefix || '',
unified_id: smsData.unified_id || '',
telegram_id: parseInt(smsData.telegram_id) || null,
channel: 'web',
session_token: smsData.session_token || sessionToken
}
};
console.log('Sending SMS payload:', payload);
// Вызываем ваш n8n workflow для отправки SMS
fetch('https://n8n.clientright.pro/webhook/2cb814ef-b376-40ad-a1bc-85bb82adfa96', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(response) {
console.log('SMS response status:', response.status);
console.log('SMS response headers:', response.headers.get('content-type'));
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.text().then(function(text) {
console.log('SMS response text:', text);
if (!text || text.trim() === '') {
throw new Error('Empty response from server');
}
try {
return JSON.parse(text);
} catch (e) {
console.error('JSON parse error:', e);
console.error('Response text:', text);
throw new Error('Invalid JSON response: ' + text);
}
});
})
.then(function(data) {
console.log('SMS parsed data:', data);
if (data && data[0] && data[0].data && data[0].data.status === 'pending') {
state.meta.smsCodeSent = true;
state.meta.smsCodeGenerated = data[0].sms_code; // Сохраняем для отладки
console.log('SMS sent successfully via n8n, code:', data[0].sms_code);
renderStatement(); // Перерендериваем для показа поля ввода кода
} else {
console.error('SMS sending failed:', data);
alert('Ошибка отправки SMS: ' + (data && data[0] && data[0].data && data[0].data.error || 'Неизвестная ошибка'));
}
})
.catch(function(error) {
console.error('SMS request failed:', error);
alert('Ошибка отправки SMS. Попробуйте еще раз.');
});
}
function verifySMS(code) {
console.log('Verifying SMS code:', code);
var smsData = injected.sms_meta || {};
var payload = [{
answer_text: code,
prefix: smsData.prefix || injected.prefix || '',
unified_id: smsData.unified_id || '',
session_token: smsData.session_token || injected.session_token || '',
channel: 'web',
telegram_id: parseInt(smsData.telegram_id || injected.telegram_id) || null,
user_id: parseInt(smsData.user_id) || null
}];
console.log('Verifying SMS payload:', payload);
// Вызываем ваш n8n workflow для проверки SMS кода
return fetch('https://n8n.clientright.pro/webhook/2b0fb3ba-99a5-4e60-aa1e-36d942329895', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log('SMS verification response:', data);
var result = data && data[0] ? data[0] : {};
if (result.success && result.verified) {
state.meta.smsVerified = true;
state.meta.smsCode = code;
console.log('SMS verified successfully via n8n');
// Убираем ошибки с поля ввода
var smsInput = document.querySelector('.sms-code-input');
if (smsInput) {
smsInput.style.borderColor = '#10b981';
smsInput.style.backgroundColor = '#f0fdf4';
}
renderStatement();
return true;
} else {
console.log('SMS verification failed:', result.error, result.message);
// Показываем ошибку пользователю
var smsInput = document.querySelector('.sms-code-input');
if (smsInput) {
smsInput.style.borderColor = '#ef4444';
smsInput.style.backgroundColor = '#fef2f2';
// Показываем сообщение об ошибке
var container = smsInput.closest('.sms-verification-container');
if (container) {
var errorMsg = container.querySelector('.sms-error-message');
if (!errorMsg) {
errorMsg = document.createElement('div');
errorMsg.className = 'sms-error-message';
errorMsg.style.cssText = 'color:#dc2626;font-size:14px;margin-top:8px;padding:8px 12px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;';
container.appendChild(errorMsg);
}
errorMsg.textContent = result.message || 'Ошибка проверки кода';
}
}
return false;
}
})
.catch(function(error) {
console.error('SMS verification request failed:', error);
alert('Ошибка проверки SMS кода. Попробуйте еще раз.');
return false;
});
}
function resendSMS() {
console.log('Resending SMS...');
state.meta.smsCodeSent = false;
state.meta.smsCode = '';
state.meta.smsVerified = false;
sendSMS();
}
// Отправка данных
function confirm() {
console.log('Confirm button clicked');
// Если согласие есть, но SMS не отправлена - отправляем SMS
if (state.meta.privacyConsent && !state.meta.smsCodeSent) {
sendSMS();
return;
}
// Если SMS не подтверждена - ничего не делаем
if (!state.meta.smsVerified) {
console.log('SMS not verified yet');
return;
}
// SMS подтверждена - отправляем заявление
console.log('Sending application...');
// Получаем токен для отправки формы
var formToken = injected.token || '';
console.log('=== ОТЛАДКА ТОКЕНА ===');
console.log('injected.token:', injected.token);
console.log('formToken:', formToken);
var payload = {
user: state.user,
project: state.project,
offenders: state.offenders,
statement_text: 'Заявление отправлено',
meta: state.meta
};
var body = {
payload: payload,
query: {
session_token: injected.session_token || '',
telegram_id: injected.telegram_id || ''
},
token: formToken,
_event: 'CONFIRM_STATEMENT'
};
var url = 'https://n8n.clientright.pro/webhook/bca73521-7cb1-4ffe-9e6d-a9ed1b7116e6';
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
.then(function(res) {
if (res.ok) {
alert('Данные отправлены успешно!');
try {
var TG = window.Telegram && window.Telegram.WebApp;
if (TG && TG.close) TG.close();
} catch(e) {}
} else {
alert('Ошибка отправки: ' + res.status);
}
})
.catch(function(e) {
alert('Ошибка: ' + e.message);
});
}
// Функция инициализации
function initialize() {
try {
console.log('=== НАЧАЛО ИНИЦИАЛИЗАЦИИ ===');
console.log('injected data:', injected);
console.log('state before render:', state);
// Проверяем, что элемент statement существует
var statementEl = document.getElementById('statement');
console.log('statement element found:', !!statementEl);
if (!statementEl) {
console.error('КРИТИЧЕСКАЯ ОШИБКА: элемент #statement не найден!');
return;
}
// Пробуем рендерить
console.log('Calling renderStatement...');
renderStatement();
console.log('renderStatement completed');
// Валидируем уже заполненные поля
setTimeout(function(){
console.log('Starting field validation...');
var fields = document.querySelectorAll('.bind');
console.log('Found fields for validation:', fields.length);
Array.prototype.forEach.call(fields, function(field){
updateFieldStyle(field);
});
console.log('Initial validation completed');
}, 100);
var confirmBtn = document.getElementById('confirmBtn');
console.log('confirmBtn found:', !!confirmBtn);
if (confirmBtn) {
confirmBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log('Confirm button clicked');
confirm();
});
}
// Делегированная фильтрация ИНН
if (!window.__innFilterAttached) {
document.addEventListener('input', function (e) {
var el = e.target;
if (!el || !el.classList) return;
var isInn = el.classList.contains('bind') && el.getAttribute('data-key') === 'inn';
if (!isInn) return;
// Логика из test/index.php - фильтрация по типу ИНН
var root = el.getAttribute('data-root');
var isIndividual = (root === 'user'); // user = физлицо, offender = юрлицо
// Оставляем только цифры
var v = (el.value || '').replace(/\D/g, '');
if (isIndividual) {
// Физлицо: максимум 12 цифр (как js-inn-mask: "999999999999")
v = v.slice(0, 12);
} else {
// Юрлицо: максимум 12 цифр, но валидны 10 или 12 (как js-inn-mask2: "9{10,12}")
v = v.slice(0, 12);
}
if (el.value !== v) el.value = v;
// Синхронизируем state
var root = el.getAttribute('data-root');
if (root === 'user') {
state.user = state.user || {};
state.user.inn = v;
} else if (root === 'offender') {
var idx = parseInt(el.getAttribute('data-index') || '0', 10);
state.offenders = state.offenders || [];
state.offenders[idx] = state.offenders[idx] || {};
state.offenders[idx].inn = v;
}
// Моментальная валидация и обновление кнопки
updateFieldStyle(el);
updateSubmitButton();
}, { passive: true });
window.__innFilterAttached = true;
}
console.log('=== ИНИЦИАЛИЗАЦИЯ ЗАВЕРШЕНА ===');
} catch (e) {
console.error('=== ОШИБКА ИНИЦИАЛИЗАЦИИ ===');
console.error('Error details:', e);
console.error('Error stack:', e.stack);
var statementEl = document.getElementById('statement');
if (statementEl) {
statementEl.innerHTML = '<p style="color:red;padding:20px;">Ошибка загрузки: ' + e.message + '<br><br>Проверьте консоль браузера (F12) для деталей.</p>';
}
}
}
// Запускаем инициализацию когда DOM готов
if (document.readyState === 'loading') {
console.log('DOM еще загружается, ждем события DOMContentLoaded...');
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded fired, starting initialization...');
initialize();
});
} else {
console.log('DOM уже готов, запускаем инициализацию сразу...');
initialize();
}
})();
</script>
</body></html>`;
// Отдаём HTML одной нодой
return [{ json: { html } }];