Files
aiform_dev/frontend/src/components/form/generateConfirmationFormHTML.ts
AI Assistant 08d59b9522 fix: Remove duplicate caseObj declaration
Problem:
- caseObj was declared twice: once at line 6 and again at line 270
- Caused esbuild compilation error

Solution:
- Removed first declaration at line 6
- Updated smsInputData extraction to handle multiple data formats
- caseObj is now only declared after normalization at line 270

Files:
- frontend/src/components/form/generateConfirmationFormHTML.ts: Fixed duplicate declaration
2025-11-24 15:59:40 +03:00

1417 lines
55 KiB
TypeScript
Raw 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.

// Функция генерации HTML формы подтверждения заявления
// Основана на структуре из n8n Code node "Mini-app Подтверждение данных"
export function generateConfirmationFormHTML(data: any): string {
// Извлекаем SMS данные (до нормализации, так как структура может быть разной)
const smsInputData = {
prefix: data.sms_meta?.prefix || data.prefix || '',
session_token: data.session_token || '',
telegram_id: data.telegram_id || '',
claim_id: data.case?.meta?.claim_id || data.claim_id || data.propertyName?.meta?.claim_id || '',
unified_id: data.case?.meta?.unified_id || data.propertyName?.meta?.unified_id || '',
user_id: data.case?.meta?.user_id || data.propertyName?.meta?.user_id || '',
status: data.case?.meta?.status || data.propertyName?.meta?.status || '',
};
// Утилиты
function safeGet(...keys: any[]): any {
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: any): any {
try {
return typeof x === 'string' ? JSON.parse(x) : x;
} catch {
return null;
}
}
function escapeHtml(str: any): string {
if (str === undefined || str === null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Нормализация денежной суммы
function normalizeMoney(rawValue: any): number | null {
if (!rawValue) return null;
let 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(',', '.');
if (!/^-?[0-9]+(\.[0-9]{1,2})?$/.test(s)) return null;
const result = parseFloat(s);
return result > 0 ? result : null;
}
// Базовые схемы
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,
};
// Функция нормализации данных из формата propertyName в формат case
function normalizeData(data: any): any {
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');
const props = data.propertyName;
const applicant = props.applicant || {};
const caseData = props.case || {};
const contract = props.contract_or_service || {};
const offenders = props.offenders || [];
const claim = props.claim || {};
const 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);
// Получаем список приложенных документов
const 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((o: any) => ({
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, {
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) {
const applicant = data.applicant || {};
const caseData = data.case || {};
const contract = data.contract_or_service || {};
const offenders = data.offenders || [];
const 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((o: any) => Object.assign({}, baseOffender, o || {})),
meta: data.meta || {},
};
}
// Если данные уже в формате case (наша текущая структура)
if (data.case) {
return data.case;
}
// Старый формат (обратная совместимость)
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((o: any) => Object.assign({}, baseOffender, o || {})) : [],
meta: Object.assign({}, data.meta || {}),
attachments: data.attachments || [],
};
}
// Нормализуем данные
let normalizedCaseObj: any;
// Если данные приходят в формате propertyName (как из n8n)
if (data.propertyName) {
normalizedCaseObj = normalizeData(data);
} else if (data.case) {
// Данные уже в формате case
normalizedCaseObj = data.case;
} else {
// Пытаемся нормализовать из любого формата
normalizedCaseObj = normalizeData(data);
}
// Убеждаемся, что есть базовые структуры
if (!normalizedCaseObj.offenders || !normalizedCaseObj.offenders.length) {
normalizedCaseObj.offenders = [Object.assign({}, baseOffender)];
}
if (!normalizedCaseObj.user) normalizedCaseObj.user = Object.assign({}, baseUser, normalizedCaseObj.user || {});
if (!normalizedCaseObj.project) normalizedCaseObj.project = Object.assign({}, baseProject, normalizedCaseObj.project || {});
if (!normalizedCaseObj.meta) normalizedCaseObj.meta = {};
if (!normalizedCaseObj.attachments) normalizedCaseObj.attachments = [];
// Нормализуем сумму, если она пришла в виде строки
if (normalizedCaseObj.project && normalizedCaseObj.project.agrprice && typeof normalizedCaseObj.project.agrprice === 'string') {
const normalized = normalizeMoney(normalizedCaseObj.project.agrprice);
if (normalized !== null) {
normalizedCaseObj.project.agrprice = normalized;
}
}
// Используем нормализованные данные
const caseObj = normalizedCaseObj;
// Сервисные поля
const sessionToken = String(safeGet(caseObj.meta?.session_token, data.session_token, ''));
const telegramId = String(safeGet(caseObj.user?.tgid, data.telegram_id, ''));
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, '')),
};
// Безопасно встраиваем данные в HTML
let caseJson = JSON.stringify({
case: caseObj,
session_token: sessionToken,
telegram_id: telegramId,
token: data.token || '',
sms_meta: smsMetaData,
});
caseJson = caseJson.replace(/</g, '\\u003c');
// 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;
}
.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;
}
.readonly-field{
background-color:#f9fafb !important;
border-color:#d1d5db !important;
color:#6b7280 !important;
cursor:not-allowed !important;
font-weight:500;
}
.date-field{
min-width:140px !important;
max-width:160px !important;
cursor:pointer;
}
.section-break{
margin:24px 0;border-top:1px solid #e5e7eb;
padding-top:16px;
}
.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;
}
.fade-in{
animation:fadeIn 0.3s ease;
}
@keyframes fadeIn{
from{opacity:0;transform:translateY(10px)}
to{opacity:1;transform:translateY(0)}
}
.buttons{
display:flex;gap:12px;margin-top:24px;
flex-wrap:wrap;
justify-content:center;
}
.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);
}
@media(max-width:768px){
.wrap{padding:16px}
.buttons{flex-direction:column}
.btn{width:100%}
.header h1{font-size:24px}
}
</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('=== СКРИПТ ЗАПУЩЕН ===');
function getData(){
try {
var dataEl = document.getElementById('case-data');
if (!dataEl) {
console.error('Элемент #case-data не найден!');
return {};
}
var textContent = dataEl.textContent || '{}';
var parsed = JSON.parse(textContent);
console.log('Parsed data:', parsed);
return parsed;
} catch(e) {
console.error('ОШИБКА ПАРСИНГА JSON:', e);
return {};
}
}
function tryParseJSON(x) {
try {
return typeof x === 'string' ? JSON.parse(x) : x;
} catch {
return null;
}
}
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;
}
// Функция нормализации данных из формата propertyName в формат case
function normalizeData(data) {
console.log('=== НОРМАЛИЗАЦИЯ ДАННЫХ ===');
console.log('Input data:', data);
// Если данные приходят в новом формате (с 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 || {};
var attachments = props.attachments_names || [];
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, {
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 || {};
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 || o.accountname || 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 || {},
};
}
// Если данные уже в формате case (наша текущая структура)
if (data.case) {
return data.case;
}
// Старый формат (обратная совместимость)
return {
user: tryParseJSON(data.user) || data.user || {},
project: tryParseJSON(data.project) || data.project || {},
offenders: Array.isArray(data.offenders) ? data.offenders : [],
meta: data.meta || {},
attachments: data.attachments || [],
};
}
function esc(s){
if (s === null || s === undefined) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
}
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 = '';
if (key === 'inn') {
var isIndividual = (root === 'user');
if (isIndividual) {
extra = ' inputmode="numeric" pattern="[0-9]{12}" maxlength="12" autocomplete="off"';
} else {
extra = ' inputmode="numeric" pattern="[0-9]{10,12}" maxlength="12" autocomplete="off"';
}
}
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 + ' />';
return fieldHtml;
}
function createReadonlyField(root, key, value) {
var id = 'field_' + root + '_' + key + '_readonly_' + Math.random().toString(36).slice(2);
return '<input class="inline-field readonly-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" value="' + esc(value || '') + '" readonly />';
}
function createDateField(root, key, value) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
var dateValue = '';
if (value) {
var cleanValue = String(value).trim();
if (cleanValue.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)) {
dateValue = cleanValue;
} else if (cleanValue.match(/^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$/)) {
var parts = cleanValue.split('.');
dateValue = parts[2] + '-' + parts[1] + '-' + parts[0];
}
}
return '<input type="date" class="inline-field bind date-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" value="' + esc(dateValue) + '" />';
}
function createMoneyField(root, key, value) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
return '<div style="display: flex; align-items: center; gap: 8px;">' +
'<input class="inline-field bind money-field" data-root="' + esc(root) + '" data-key="' + esc(key) + '"' +
' id="' + id + '" value="' + esc(value || '') + '"' +
' inputmode="decimal" pattern="[0-9]*[.,]?[0-9]*" autocomplete="off" />' +
'<span style="color: #6b7280; font-size: 14px;">рублей</span>' +
'</div>';
}
function createTextarea(root, key, value) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
return '<textarea class="inline-field bind full-width" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '">' + esc(value || '') + '</textarea>';
}
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>';
return checkboxHtml;
}
// Получаем данные
var injected = getData();
console.log('=== ПОЛУЧЕНЫ ДАННЫЕ ===');
console.log('injected:', injected);
console.log('injected.case:', injected.case);
console.log('injected.propertyName:', injected.propertyName);
// Достаём объект кейса из «типичных» мест
var dataCandidate = null;
if (!dataCandidate && injected.propertyName !== undefined) {
// Если propertyName - это объект (как в вашем случае), берем его напрямую
if (typeof injected.propertyName === 'object' && injected.propertyName !== null) {
dataCandidate = injected.propertyName;
} else if (typeof injected.propertyName === 'string') {
dataCandidate = tryParseJSON(injected.propertyName);
}
}
if (!dataCandidate && injected.value !== undefined) dataCandidate = tryParseJSON(injected.value);
if (!dataCandidate && (injected.user || injected.project || injected.offenders || injected.meta)) dataCandidate = injected;
if (!dataCandidate && injected.data) dataCandidate = injected.data;
if (!dataCandidate && injected.output) dataCandidate = tryParseJSON(injected.output) || injected.output;
if (!dataCandidate && injected.case) dataCandidate = { case: injected.case };
dataCandidate = dataCandidate || injected;
console.log('dataCandidate:', dataCandidate);
console.log('Type of dataCandidate:', typeof dataCandidate);
console.log('Keys of dataCandidate:', Object.keys(dataCandidate || {}));
// Если dataCandidate - массив, берем первый элемент
var dataToNormalize = Array.isArray(dataCandidate) ? dataCandidate[0] : dataCandidate;
console.log('Data to normalize:', dataToNormalize);
// Нормализуем данные
var normalizedCase = normalizeData(dataToNormalize);
console.log('Normalized case:', normalizedCase);
// Формируем state из нормализованных данных
var state = normalizedCase || { user: {}, project: {}, offenders: [{}], meta: {}, attachments: [] };
// Убеждаемся, что есть базовые структуры
if (!state.offenders || !state.offenders.length) {
state.offenders = [{}];
}
if (!state.user) state.user = {};
if (!state.project) state.project = {};
if (!state.meta) state.meta = {};
if (!state.attachments) state.attachments = [];
// Добавляем SMS данные из injected
if (injected.sms_meta) {
state.meta = Object.assign({}, state.meta, injected.sms_meta);
}
if (injected.session_token) {
state.meta.session_token = injected.session_token;
}
if (injected.token) {
state.meta.token = injected.token;
}
console.log('Final state:', state);
function renderStatement() {
var u = state.user || {};
var o = (state.offenders && state.offenders[0]) || {};
var p = state.project || {};
var html = '';
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 && state.meta.privacyConsent,
'Я ознакомлен(а) и согласен(а) с <a href="https://clientright.ru/person" target="_blank">Политикой обработки персональных данных</a> и даю согласие на обработку моих персональных данных в соответствии с Федеральным законом от 27.07.2006 №152-ФЗ «О персональных данных»',
true);
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>';
}
var statementEl = document.getElementById('statement');
if (statementEl) {
console.log('Setting innerHTML, HTML length:', html.length);
statementEl.innerHTML = html;
console.log('innerHTML set, calling attachBindings...');
attachBindings();
console.log('attachBindings completed');
// Обновляем состояние кнопки отправки
setTimeout(function() {
updateSubmitButton();
}, 100);
console.log('=== РЕНДЕРИНГ ЗАВЕРШЕН УСПЕШНО ===');
} else {
console.error('ОШИБКА: элемент #statement не найден в renderStatement!');
}
}
// Простые функции валидации
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) {
if (!/^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$/.test(s)) return null;
var [d,m,y] = s.split('.').map(Number);
var dt = new Date(y, m-1, d);
var isValid = (dt.getFullYear()===y && dt.getMonth()===m-1 && dt.getDate()===d);
return isValid ? dt : null;
}
function parseYMD(s) {
var cleanS = s.trim().replace(/[\u2012\u2013\u2014\u2015\u2212\uFF0D]/g, '-');
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(cleanS)) return null;
var [y,m,d] = cleanS.split('-').map(Number);
var dt = new Date(y, m-1, d);
var isValid = (dt.getFullYear()===y && dt.getMonth()===m-1 && dt.getDate()===d);
return isValid ? dt : null;
}
function toYMD(dt) {
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 isValidINN(inn, isIndividual) {
if (!inn) return false;
var clean = String(inn).replace(/\D/g, '');
if (isIndividual) {
return clean.length === 12;
} else {
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);
return hasCyrillic && !hasInvalidChars;
}
function isValidAddress(address) {
if (!address) return false;
var trimmed = address.trim();
if (trimmed.length < 10) return false;
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;
return parseFloat(s) > 0;
}
// Функция проверки всех обязательных полей
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 validationErrors = validateAllFields();
if (!isConsentGiven) {
confirmBtn.disabled = true;
confirmBtn.style.opacity = '0.6';
confirmBtn.style.cursor = 'not-allowed';
confirmBtn.textContent = '✅ Подтвердить и отправить';
} else 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 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, '.');
}
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;
// Обновляем телефон в СБП
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;
} else if (root === 'offender') {
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();
}
}
// Обновляем состояние кнопки при изменении любого поля
updateSubmitButton();
});
// Блокировка нечисловых символов для ИНН
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();
}
}
});
// Фильтрация при вставке для ИНН
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;
cleanPaste = cleanPaste.slice(0, maxLength);
this.value = cleanPaste;
var inputEvent = new Event('input', { bubbles: true });
this.dispatchEvent(inputEvent);
}
});
// Валидация при потере фокуса
field.addEventListener('blur', function() {
updateSubmitButton();
});
// Дополнительная обработка для чекбоксов
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');
}
}
}
});
}
});
}
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;
}
// Инициализируем поля для шаблонов, если их нет
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;
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){
// Валидация будет выполнена при первом взаимодействии
});
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');
// Проверяем согласие
if (!state.meta || !state.meta.privacyConsent) {
alert('Необходимо дать согласие на обработку персональных данных');
return;
}
// Проверяем валидность полей
var validationErrors = validateAllFields();
if (validationErrors.length > 0) {
alert('Заполните все обязательные поля: ' + validationErrors.join(', '));
return;
}
window.parent.postMessage({
type: 'claim_confirmed',
data: {
user: state.user,
project: state.project,
offenders: state.offenders,
meta: state.meta
},
originalData: injected
}, '*');
});
}
// Делегированная фильтрация ИНН
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;
}
// Моментальная валидация и обновление кнопки
updateSubmitButton();
}, { passive: true });
window.__innFilterAttached = true;
}
// Отправляем сообщение родителю при загрузке
window.parent.postMessage({type: 'claim_form_loaded'}, '*');
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>`;
return html;
}