diff --git a/include/Webservices/CreateWebContact.php b/include/Webservices/CreateWebContact.php index 667e2855..a4aa24dc 100644 --- a/include/Webservices/CreateWebContact.php +++ b/include/Webservices/CreateWebContact.php @@ -22,7 +22,7 @@ vimport ('includes.runtime.LanguageHandler'); * @param string $firstname - имя (опционально) * @param string $lastname - фамилия (опционально) * @param string $email - email (опционально) - * @return int - ID контакта + * @return string - JSON строка с contact_id, is_new и cf_2624 (Данные подтверждены) */ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email = '', $user = false) { @@ -56,18 +56,29 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email $isNew = false; // Флаг: создан ли контакт сейчас // Проверяем существование контакта по номеру телефона - $query = "select c.contactid + // ✅ Добавляем выборку поля cf_2624 (Данные подтверждены) + $query = "select c.contactid, cf.cf_2624 from vtiger_contactdetails c - left join vtiger_crmentity e on e.crmid = c.contactid + left join vtiger_crmentity e on e.crmid = c.contactid + left join vtiger_contactscf cf on cf.contactid = c.contactid where e.deleted = 0 and c.mobile = ? limit 1"; $result = $adb->pquery($query, array($mobile)); + $cf_2624_value = "0"; // По умолчанию "Нет" (данные не подтверждены) + if ($adb->num_rows($result) > 0) { // Контакт существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!) $output = $adb->query_result($result, 0, 'contactid'); $isNew = false; - $logstring = date('Y-m-d H:i:s').' ✅ Контакт найден с id '.$output.' (БЕЗ обновления)'.PHP_EOL; + + // ✅ Получаем значение поля cf_2624 (Данные подтверждены) + $cf_2624_value = $adb->query_result($result, 0, 'cf_2624'); + if (empty($cf_2624_value)) { + $cf_2624_value = "0"; // По умолчанию "Нет" + } + + $logstring = date('Y-m-d H:i:s').' ✅ Контакт найден с id '.$output.', cf_2624='.$cf_2624_value.' (БЕЗ обновления)'.PHP_EOL; file_put_contents('logs/CreateWebContact.log', $logstring, FILE_APPEND); } else { // Контакт НЕ существует - создаём новый @@ -92,6 +103,7 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email 'mailingstreet' => '', // Адрес пустой 'cf_1849' => '', // Реквизиты пустые 'cf_1580' => '', // Код пустой + 'cf_2624' => '0', // ✅ Данные подтверждены = "Нет" (по умолчанию для новых контактов) 'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id) ); @@ -102,7 +114,8 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email $contact = vtws_create('Contacts', $params, $current_user); $output = substr($contact['id'], 3); $isNew = true; // Контакт только что создан! - $logstring = date('Y-m-d H:i:s').' ✅ Создан новый Web Контакт с id '.$output.PHP_EOL; + $cf_2624_value = "0"; // Новый контакт - данные не подтверждены + $logstring = date('Y-m-d H:i:s').' ✅ Создан новый Web Контакт с id '.$output.', cf_2624=0'.PHP_EOL; file_put_contents('logs/CreateWebContact.log', $logstring, FILE_APPEND); } catch (WebServiceException $ex) { $logstring = date('Y-m-d H:i:s').' ❌ Ошибка создания: '.$ex->getMessage().PHP_EOL; @@ -111,10 +124,11 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email } } - // Возвращаем JSON с флагом is_new + // Возвращаем JSON с флагом is_new и значением cf_2624 $result = array( 'contact_id' => $output, - 'is_new' => $isNew + 'is_new' => $isNew, + 'cf_2624' => $cf_2624_value // ✅ "1" = данные подтверждены, "0" = не подтверждены ); $logstring = date('Y-m-d H:i:s').' Return: '.json_encode($result).PHP_EOL; diff --git a/storage/2025/December/week1/399640_Ходатайство_по_делу_.pdf b/storage/2025/December/week1/399640_Ходатайство_по_делу_.pdf new file mode 100644 index 00000000..2d976a35 Binary files /dev/null and b/storage/2025/December/week1/399640_Ходатайство_по_делу_.pdf differ diff --git a/storage/2025/December/week1/399643_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/December/week1/399643_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..b1abad40 Binary files /dev/null and b/storage/2025/December/week1/399643_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/December/week1/399706_AgACAgIAAxkBAAEBcgdpMCjdJMlJRSYETr2N6WW3gskgQAACHQ9rG4E_gUknG71mZ5TEogEAAwIAA3kAAzYE.pdf b/storage/2025/December/week1/399706_AgACAgIAAxkBAAEBcgdpMCjdJMlJRSYETr2N6WW3gskgQAACHQ9rG4E_gUknG71mZ5TEogEAAwIAA3kAAzYE.pdf new file mode 100644 index 00000000..e10e6640 Binary files /dev/null and b/storage/2025/December/week1/399706_AgACAgIAAxkBAAEBcgdpMCjdJMlJRSYETr2N6WW3gskgQAACHQ9rG4E_gUknG71mZ5TEogEAAwIAA3kAAzYE.pdf differ diff --git a/test/LanguageManager/Workflow2 b/test/LanguageManager/Workflow2 index a6ef99c5..ec8964e4 100644 --- a/test/LanguageManager/Workflow2 +++ b/test/LanguageManager/Workflow2 @@ -1 +1 @@ -2025-12-02 15:00:08 \ No newline at end of file +2025-12-03 15:05:09 \ No newline at end of file diff --git a/ticket_form/SESSION_LOG_2025-12-03.md b/ticket_form/SESSION_LOG_2025-12-03.md new file mode 100644 index 00000000..975e3a14 --- /dev/null +++ b/ticket_form/SESSION_LOG_2025-12-03.md @@ -0,0 +1,132 @@ +# Лог сессии 2025-12-03 + +## Задача: Получение cf_2624 из MySQL при загрузке черновика + +### Проблема +Пользователь заметил, что для `claim_id: "226564ce-d7cf-48ee-a820-690e8f5ec8e5"` доступно редактирование, хотя в CRM стоит галка "Данные подтверждены" (`cf_2624 = "1"`). + +### Решение +Вместо передачи `cf_2624` через события Redis, реализован прямой SQL запрос к MySQL БД vtiger CRM при загрузке черновика. + +## Изменения + +### 1. Добавлены credentials для MySQL CRM в `config.py` +```python +# MySQL CRM (vtiger CRM) +mysql_crm_host: str = "localhost" +mysql_crm_port: int = 3306 +mysql_crm_db: str = "ci20465_72new" +mysql_crm_user: str = "ci20465_72new" +mysql_crm_password: str = "EcY979Rn" +``` + +### 2. Создан сервис `CrmMySQLService` +**Файл:** `ticket_form/backend/app/services/crm_mysql_service.py` + +- Подключение к MySQL БД vtiger CRM +- Методы: `fetch_one()`, `fetch_all()`, `execute()` +- Использует `aiomysql` для асинхронных запросов + +### 3. Обновлён `main.py` +- Добавлено подключение к MySQL CRM при старте +- Добавлено закрытие соединения при остановке + +### 4. Обновлён `claims.py` - метод `get_draft()` +**Эндпоинт:** `GET /api/v1/claims/drafts/{claim_id}` + +**Изменения:** +- Убран webservice API (getchallenge → login → retrieve) +- Добавлен прямой SQL запрос к MySQL для получения `cf_2624` +- Получаем все данные контакта, включая `cf_2624` +- Добавлено логирование для отладки + +**SQL запрос:** +```sql +SELECT + cd.contactid, + cd.firstname, + cd.lastname, + cd.email, + cd.mobile, + ccf.cf_2624 AS cf_2624 +FROM vtiger_contactdetails cd +LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid +LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid +WHERE cd.contactid = %s + AND ce.deleted = 0 +LIMIT 1 +``` + +**Логика:** +- Если `cf_2624 = "1"` → `contact_data_confirmed = True`, `contact_data_can_edit = False` +- Если `cf_2624 = "0"` или `NULL` → `contact_data_confirmed = False`, `contact_data_can_edit = True` + +### 5. Обновлены SQL файлы и документация +- `N8N_POSTGRESQL_GET_CONTACT_DATA.sql` → `N8N_MYSQL_GET_CONTACT_DATA.sql` +- Изменён синтаксис: `$1` → `?` (для n8n MySQL ноды) +- Обновлена документация `BACKEND_GET_CONTACT_CF_2624_FROM_POSTGRESQL.md` +- Создан `N8N_MYSQL_GET_CONTACT_DATA.md` + +## Преимущества нового подхода + +1. ✅ **Проще** - один SQL запрос вместо цепочки HTTP запросов +2. ✅ **Быстрее** - прямой запрос к БД +3. ✅ **Надёжнее** - не зависит от webservice API +4. ✅ **Актуальнее** - всегда получаем свежие данные из БД + +## Проверка + +**MySQL запрос:** +```bash +mysql -h localhost -u ci20465_72new -p'EcY979Rn' ci20465_72new \ + -e "SELECT contactid, cf_2624 FROM vtiger_contactscf WHERE contactid = '399542' LIMIT 1;" +``` + +**Результат:** +``` +contactid cf_2624 +399542 1 +``` + +✅ В MySQL `cf_2624 = "1"` для `contact_id = "399542"` - данные подтверждены. + +## Текущий статус + +- ✅ Код обновлён +- ✅ Бэкенд перезапущен +- ⚠️ Требуется проверка: почему в ответе API `contact_data_confirmed` и `contact_data_can_edit` равны `null` + +**Возможные причины:** +1. Запрос к MySQL не выполняется (нет логов) +2. `contact_id` не передаётся или имеет неправильный тип +3. Ошибка в SQL запросе (не логируется) + +**Добавлено логирование:** +```python +logger.info(f"🔍 Получен contact_id из черновика: {contact_id} (type: {type(contact_id)})") +``` + +## Следующие шаги + +1. Проверить логи при загрузке черновика +2. Убедиться, что `contact_id` передаётся корректно +3. Проверить, что SQL запрос выполняется успешно +4. Убедиться, что фронтенд правильно использует `contact_data_confirmed` для блокировки полей + +--- + +## Файлы изменены + +- `ticket_form/backend/app/config.py` - добавлены credentials для MySQL CRM +- `ticket_form/backend/app/services/crm_mysql_service.py` - новый сервис +- `ticket_form/backend/app/main.py` - подключение к MySQL CRM +- `ticket_form/backend/app/api/claims.py` - прямой SQL запрос к MySQL +- `ticket_form/docs/N8N_MYSQL_GET_CONTACT_DATA.sql` - SQL запрос для n8n +- `ticket_form/docs/N8N_MYSQL_GET_CONTACT_DATA.md` - документация +- `ticket_form/docs/BACKEND_GET_CONTACT_CF_2624_FROM_POSTGRESQL.md` - обновлена документация + +--- + +**Время работы:** 2025-12-03 16:00-16:30 +**Статус:** В процессе отладки + diff --git a/ticket_form/backend/app/api/claims.py b/ticket_form/backend/app/api/claims.py index 9d2d3735..2bc8871d 100644 --- a/ticket_form/backend/app/api/claims.py +++ b/ticket_form/backend/app/api/claims.py @@ -15,6 +15,7 @@ import json import logging from ..services.redis_service import redis_service from ..services.database import db +from ..services.crm_mysql_service import crm_mysql_service from ..config import settings router = APIRouter(prefix="/api/v1/claims", tags=["Claims"]) @@ -456,18 +457,114 @@ async def get_draft(claim_id: str): if documents_required: logger.info(f"🔍 documents_required: {documents_required[:2]}...") # Первые 2 для примера + # ✅ Проверяем флаг подтверждения данных контакта из CRM (поле cf_2624) + # Простой способ: делаем прямой SQL запрос к БД (таблицы vtiger_*) + # ПРИМЕЧАНИЕ: Если таблицы vtiger_* находятся в MySQL (а не PostgreSQL), + # нужно использовать отдельный connection через policy_service или создать новый MySQL connection + unified_id = row.get('unified_id') + contact_data_confirmed = False + contact_data_can_edit = True + contact_data_from_crm = None + + # Получаем contact_id из payload + contact_id = payload.get('contact_id') if isinstance(payload, dict) else None + + # Преобразуем contact_id в строку, если он есть + if contact_id: + contact_id = str(contact_id).strip() + logger.info(f"🔍 Получен contact_id из черновика: {contact_id} (type: {type(contact_id)})") + + if contact_id: + try: + # ✅ Прямой SQL запрос к MySQL для получения cf_2624 + # Таблицы vtiger_* находятся в MySQL БД + contact_query = """ + SELECT + cd.contactid, + cd.firstname, + cd.lastname, + cd.email, + cd.mobile, + cd.phone, + cs.birthday, + ca.mailingstreet, + ca.mailingcity, + ca.mailingstate, + ca.mailingzip, + ca.mailingcountry, + ccf.cf_1157 AS middle_name, + ccf.cf_1263 AS birthplace, + ccf.cf_1257 AS inn, + ccf.cf_1849 AS requisites, + ccf.cf_1580 AS code, + ccf.cf_1706 AS sms, + ccf.cf_2624 AS cf_2624 + FROM vtiger_contactdetails cd + LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid + LEFT JOIN vtiger_contactsubdetails cs ON cs.contactsubscriptionid = cd.contactid + LEFT JOIN vtiger_contactaddress ca ON ca.contactaddressid = cd.contactid + LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid + WHERE cd.contactid = %s + AND ce.deleted = 0 + LIMIT 1 + """ + + contact_row = await crm_mysql_service.fetch_one(contact_query, contact_id) + + if contact_row: + # Формируем объект с данными контакта + contact_data_from_crm = { + "contactid": contact_row.get("contactid"), + "firstname": contact_row.get("firstname"), + "lastname": contact_row.get("lastname"), + "email": contact_row.get("email"), + "mobile": contact_row.get("mobile"), + "phone": contact_row.get("phone"), + "birthday": contact_row.get("birthday"), + "mailingstreet": contact_row.get("mailingstreet"), + "mailingcity": contact_row.get("mailingcity"), + "mailingstate": contact_row.get("mailingstate"), + "mailingzip": contact_row.get("mailingzip"), + "mailingcountry": contact_row.get("mailingcountry"), + "cf_1157": contact_row.get("middle_name"), # Отчество + "cf_1263": contact_row.get("birthplace"), # Место рождения + "cf_1257": contact_row.get("inn"), # ИНН + "cf_1849": contact_row.get("requisites"), # Реквизиты + "cf_1580": contact_row.get("code"), # Код + "cf_1706": contact_row.get("sms"), # SMS + "cf_2624": contact_row.get("cf_2624") or "0" # ✅ Данные подтверждены + } + + # ✅ Проверяем кастомное поле "Данные подтверждены" (cf_2624) + confirmed_field = contact_data_from_crm.get("cf_2624", "0") + contact_data_confirmed = confirmed_field == "1" or confirmed_field == "true" or confirmed_field is True + contact_data_can_edit = not contact_data_confirmed + + logger.info( + f"🔒 Статус данных контакта из MySQL CRM: confirmed={contact_data_confirmed}, " + f"field_value={confirmed_field}, contact_id={contact_id}" + ) + else: + logger.warning(f"⚠️ Контакт не найден в MySQL CRM: contact_id={contact_id}") + except Exception as e: + logger.warning(f"⚠️ Не удалось загрузить данные контакта из MySQL CRM: {str(e)}") + return { "success": True, "claim": { "id": str(row['id']), - "claim_id": final_claim_id, # ✅ Используем claim_id из payload, если его нет в row + "claim_id": final_claim_id, "session_token": row.get('session_token'), "status_code": row.get('status_code'), - "channel": row.get('channel'), # ✅ Добавляем channel для отладки + "channel": row.get('channel'), "created_at": row['created_at'].isoformat() if row.get('created_at') else None, "updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None, "payload": payload - } + }, + # ✅ Флаги подтверждения данных контакта (из CRM поля cf_2624) + "contact_data_confirmed": contact_data_confirmed, + "contact_data_can_edit": contact_data_can_edit, + "contact_data_from_crm": contact_data_from_crm # Данные из CRM (всегда загружаем, если есть contact_id) } except HTTPException: diff --git a/ticket_form/backend/app/api/events.py b/ticket_form/backend/app/api/events.py index 7fe452c9..3ee1057d 100644 --- a/ticket_form/backend/app/api/events.py +++ b/ticket_form/backend/app/api/events.py @@ -218,19 +218,41 @@ async def stream_events(task_id: str): # ✅ Обработка ocr_status ready: загружаем form_draft из PostgreSQL if actual_event.get('event_type') == 'ocr_status' and actual_event.get('status') == 'ready': claim_id = actual_event.get('claim_id') or actual_event.get('data', {}).get('claim_id') + # ✅ Получаем cf_2624 из события (Данные подтверждены) + cf_2624 = actual_event.get('cf_2624') + if claim_id: - logger.info(f"🔍 OCR ready event received, loading form_draft for claim_id={claim_id}") + logger.info(f"🔍 OCR ready event received, loading form_draft for claim_id={claim_id}, cf_2624={cf_2624}") try: + # ✅ Если есть cf_2624 в событии - сохраняем в черновик + if cf_2624 is not None: + try: + update_query = """ + UPDATE clpr_claims + SET payload = jsonb_set( + COALESCE(payload, '{}'::jsonb), + '{cf_2624}', + $1::jsonb + ) + WHERE id::text = $2 OR payload->>'claim_id' = $2 + RETURNING id; + """ + await db.execute(update_query, json.dumps(cf_2624), claim_id) + logger.info(f"✅ Сохранён cf_2624={cf_2624} в черновик claim_id={claim_id}") + except Exception as e: + logger.warning(f"⚠️ Не удалось сохранить cf_2624 в черновик: {e}") + # Загружаем form_draft и documents из PostgreSQL query = """ SELECT c.id, c.payload->'form_draft' as form_draft, c.payload->'documents_required' as documents_required, - c.payload->'documents_meta' as documents_meta + c.payload->'documents_meta' as documents_meta, + c.payload->>'cf_2624' as cf_2624 FROM clpr_claims c - WHERE c.id::text = $1 + WHERE c.id::text = $1 OR c.payload->>'claim_id' = $1 LIMIT 1 """ @@ -241,6 +263,7 @@ async def stream_events(task_id: str): form_draft_raw = row.get('form_draft') documents_required_raw = row.get('documents_required') documents_meta_raw = row.get('documents_meta') + cf_2624_from_db = row.get('cf_2624') # ✅ Получаем cf_2624 из БД # Парсим если строка def parse_json_field(val): @@ -266,7 +289,10 @@ async def stream_events(task_id: str): 'documents_meta': documents_meta, } - logger.info(f"✅ Form draft loaded from PostgreSQL for claim_id={claim_id}, has_form_draft={form_draft is not None}") + # ✅ Добавляем cf_2624 в событие (из БД или из события) + actual_event['cf_2624'] = cf_2624_from_db or cf_2624 or "0" + + logger.info(f"✅ Form draft loaded from PostgreSQL for claim_id={claim_id}, has_form_draft={form_draft is not None}, cf_2624={actual_event.get('cf_2624')}") else: logger.warning(f"⚠️ Claim not found in PostgreSQL: claim_id={claim_id}") except Exception as e: diff --git a/ticket_form/backend/app/config.py b/ticket_form/backend/app/config.py index c9f2cd26..e75c9c0f 100644 --- a/ticket_form/backend/app/config.py +++ b/ticket_form/backend/app/config.py @@ -42,6 +42,15 @@ class Settings(BaseSettings): mysql_user: str = "root" mysql_password: str = "" + # ============================================ + # MYSQL CRM (vtiger CRM) + # ============================================ + mysql_crm_host: str = "localhost" + mysql_crm_port: int = 3306 + mysql_crm_db: str = "ci20465_72new" + mysql_crm_user: str = "ci20465_72new" + mysql_crm_password: str = "EcY979Rn" + @property def database_url(self) -> str: """Формирует URL для подключения к PostgreSQL""" diff --git a/ticket_form/backend/app/main.py b/ticket_form/backend/app/main.py index 1a51a5db..6bdf28d0 100644 --- a/ticket_form/backend/app/main.py +++ b/ticket_form/backend/app/main.py @@ -11,6 +11,7 @@ from .services.database import db from .services.redis_service import redis_service from .services.rabbitmq_service import rabbitmq_service from .services.policy_service import policy_service +from .services.crm_mysql_service import crm_mysql_service from .services.s3_service import s3_service from .api import sms, claims, policy, upload, draft, events, n8n_proxy, session, documents @@ -56,6 +57,12 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"⚠️ MySQL Policy DB not available: {e}") + try: + # Подключаем MySQL CRM (vtiger) + await crm_mysql_service.connect() + except Exception as e: + logger.warning(f"⚠️ MySQL CRM DB not available: {e}") + try: # Подключаем S3 (для загрузки файлов) s3_service.connect() @@ -73,6 +80,7 @@ async def lifespan(app: FastAPI): await redis_service.disconnect() await rabbitmq_service.disconnect() await policy_service.close() + await crm_mysql_service.close() logger.info("👋 Ticket Form Intake Platform stopped") diff --git a/ticket_form/backend/app/services/crm_mysql_service.py b/ticket_form/backend/app/services/crm_mysql_service.py new file mode 100644 index 00000000..a6ba8e83 --- /dev/null +++ b/ticket_form/backend/app/services/crm_mysql_service.py @@ -0,0 +1,117 @@ +""" +CRM MySQL Service - Подключение к MySQL БД vtiger CRM +""" +import aiomysql +from typing import Optional, Dict, Any, List +from ..config import settings +import logging + +logger = logging.getLogger(__name__) + + +class CrmMySQLService: + """Сервис для работы с MySQL БД vtiger CRM""" + + def __init__(self): + self.pool: Optional[aiomysql.Pool] = None + + async def connect(self): + """Подключение к MySQL БД vtiger CRM""" + try: + self.pool = await aiomysql.create_pool( + host=settings.mysql_crm_host, + port=settings.mysql_crm_port, + user=settings.mysql_crm_user, + password=settings.mysql_crm_password, + db=settings.mysql_crm_db, + autocommit=True, + minsize=1, + maxsize=5 + ) + logger.info(f"✅ MySQL CRM DB connected: {settings.mysql_crm_host}:{settings.mysql_crm_port}/{settings.mysql_crm_db}") + except Exception as e: + logger.error(f"❌ MySQL CRM DB connection error: {e}") + raise + + async def fetch_one(self, query: str, *args) -> Optional[Dict[str, Any]]: + """ + Выполнить SQL запрос и вернуть одну запись + + Args: + query: SQL запрос с плейсхолдерами %s + *args: Параметры для запроса + + Returns: + Dict с данными или None если не найдено + """ + if not self.pool: + await self.connect() + + try: + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(query, args) + result = await cursor.fetchone() + return dict(result) if result else None + except Exception as e: + logger.error(f"❌ Error executing query: {e}") + raise + + async def fetch_all(self, query: str, *args) -> List[Dict[str, Any]]: + """ + Выполнить SQL запрос и вернуть все записи + + Args: + query: SQL запрос с плейсхолдерами %s + *args: Параметры для запроса + + Returns: + List[Dict] с данными + """ + if not self.pool: + await self.connect() + + try: + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(query, args) + results = await cursor.fetchall() + return [dict(row) for row in results] if results else [] + except Exception as e: + logger.error(f"❌ Error executing query: {e}") + raise + + async def execute(self, query: str, *args) -> int: + """ + Выполнить SQL запрос (INSERT, UPDATE, DELETE) + + Args: + query: SQL запрос с плейсхолдерами %s + *args: Параметры для запроса + + Returns: + Количество затронутых строк + """ + if not self.pool: + await self.connect() + + try: + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.execute(query, args) + return cursor.rowcount + except Exception as e: + logger.error(f"❌ Error executing query: {e}") + raise + + async def close(self): + """Закрыть пул подключений""" + if self.pool: + self.pool.close() + await self.pool.wait_closed() + logger.info("MySQL CRM DB pool closed") + + +# Глобальный экземпляр +crm_mysql_service = CrmMySQLService() + diff --git a/ticket_form/docs/BACKEND_GET_CONTACT_CF_2624_FROM_POSTGRESQL.md b/ticket_form/docs/BACKEND_GET_CONTACT_CF_2624_FROM_POSTGRESQL.md new file mode 100644 index 00000000..3a931b84 --- /dev/null +++ b/ticket_form/docs/BACKEND_GET_CONTACT_CF_2624_FROM_POSTGRESQL.md @@ -0,0 +1,97 @@ +# Получение cf_2624 из MySQL при загрузке черновика + +## ✅ Упрощённый подход + +Вместо передачи `cf_2624` через события Redis, просто делаем прямой SQL запрос к MySQL при загрузке черновика. + +## Где это происходит + +**Файл:** `ticket_form/backend/app/api/claims.py` +**Эндпоинт:** `GET /api/v1/claims/drafts/{claim_id}` +**Функция:** `get_draft()` + +## Как работает + +1. **Получаем `contact_id` из черновика:** + ```python + contact_id = payload.get('contact_id') + ``` + +2. **Делаем SQL запрос к MySQL:** + ```sql + SELECT + cd.contactid, + cd.firstname, + cd.lastname, + cd.email, + cd.mobile, + ccf.cf_2624 AS cf_2624 + FROM vtiger_contactdetails cd + LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid + LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid + WHERE cd.contactid = %s + AND ce.deleted = 0 + LIMIT 1 + ``` + +3. **Используем `cf_2624` для блокировки полей:** + ```python + contact_data_confirmed = (cf_2624 == "1") + contact_data_can_edit = not contact_data_confirmed + ``` + +## Преимущества + +1. ✅ **Проще** - один SQL запрос вместо цепочки событий +2. ✅ **Быстрее** - прямой запрос к БД +3. ✅ **Надёжнее** - не зависит от событий Redis +4. ✅ **Актуальнее** - всегда получаем свежие данные из БД + +## Что не нужно делать + +- ❌ Передавать `cf_2624` через события Redis +- ❌ Сохранять `cf_2624` в черновик при обработке событий +- ❌ Использовать webservice API для получения `cf_2624` + +## Проверка + +1. ✅ При загрузке черновика делается SQL запрос к PostgreSQL +2. ✅ Получаем `cf_2624` из таблицы `vtiger_contactscf` +3. ✅ Используем для блокировки полей на фронтенде + +--- + +## Реализация + +### MySQL Connection для CRM + +Создан отдельный сервис `CrmMySQLService` для подключения к MySQL БД vtiger CRM: + +**Файл:** `ticket_form/backend/app/services/crm_mysql_service.py` + +**Credentials (из config.php):** +- Host: `localhost` +- Port: `3306` +- Database: `ci20465_72new` +- User: `ci20465_72new` +- Password: `EcY979Rn` + +### Использование в коде + +```python +from ..services.crm_mysql_service import crm_mysql_service + +# SQL запрос с MySQL синтаксисом (%s вместо $1) +contact_query = """ +SELECT ... FROM vtiger_contactdetails cd +WHERE cd.contactid = %s +""" +contact_row = await crm_mysql_service.fetch_one(contact_query, contact_id) +``` + +### Отличия от PostgreSQL + +- Параметры: `%s` вместо `$1` +- Синтаксис JOIN: тот же +- LIMIT: тот же + diff --git a/ticket_form/docs/CF_2624_IMPLEMENTATION_SUMMARY.md b/ticket_form/docs/CF_2624_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..b19ebf4d --- /dev/null +++ b/ticket_form/docs/CF_2624_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,135 @@ +# Реализация проверки cf_2624 при формировании заявления + +## ✅ Что сделано + +### 1. Backend API (`/drafts/{claim_id}`) +- ✅ Получает `cf_2624` из CRM через webservice `retrieve` +- ✅ Преобразует в `contact_data_confirmed` (boolean) +- ✅ Возвращает в ответе API вместе с `contact_data_from_crm` + +**Файл:** `ticket_form/backend/app/api/claims.py` (строки 459-539) + +### 2. Frontend - Загрузка черновика +- ✅ Получает `contact_data_confirmed` из ответа API +- ✅ Сохраняет в `formData` +- ✅ Передаёт в `claimPlanData` для `StepClaimConfirmation` + +**Файл:** `ticket_form/frontend/src/pages/ClaimForm.tsx` (строки 564-848) + +### 3. Frontend - Форма подтверждения +- ✅ `StepClaimConfirmation` получает `contact_data_confirmed` из `claimPlanData` +- ✅ Передаёт в `generateConfirmationFormHTML` +- ✅ Форма блокирует персональные данные если `contact_data_confirmed = true` + +**Файлы:** +- `ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx` (строки 89-96) +- `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` (строки 4, 293, 724-740, 840, 907-915) + +### 4. CreateWebContact +- ✅ Возвращает `cf_2624` в JSON ответе +- ✅ Для новых контактов: `cf_2624 = "0"` +- ✅ Для существующих: берёт значение из CRM + +**Файл:** `include/Webservices/CreateWebContact.php` + +--- + +## ⏳ Что нужно сделать + +### 1. Обновить n8n workflow `6mxRJ2LLHmQXyaDz` + +**После ноды `CreateWebContacКлиентправ`:** + +Добавить ноду `Code: Extract Contact Data Confirmed`: + +```javascript +// Парсим результат CreateWebContact +const rawResult = $node["CreateWebContacКлиентправ"].json.result; +const contactData = JSON.parse(rawResult); + +// Извлекаем cf_2624 (Данные подтверждены) +const cf_2624 = contactData.cf_2624 || "0"; +const contact_data_confirmed = cf_2624 === "1"; + +return { + contact_id: contactData.contact_id, + is_new_contact: contactData.is_new, + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: !contact_data_confirmed +}; +``` + +**В ноде `Code in JavaScriptКлиентправ` (формирование ответа):** + +Добавить в return: + +```javascript +const contactStatus = $('Code: Extract Contact Data Confirmed').first().json; + +return { + // ... существующие поля ... + contact_data_confirmed: contactStatus.contact_data_confirmed || false, + contact_data_can_edit: contactStatus.contact_data_can_edit !== false, + cf_2624: contactStatus.cf_2624 || "0", + // ... остальные поля ... +}; +``` + +**См. подробности:** `ticket_form/docs/N8N_UPDATE_CF_2624_IN_RESPONSE.md` + +--- + +## 🔄 Логика работы + +### Сценарий 1: Загрузка черновика +1. Пользователь выбирает черновик +2. Frontend вызывает `/api/v1/claims/drafts/{claim_id}` +3. Backend получает `cf_2624` из CRM +4. Backend возвращает `contact_data_confirmed = (cf_2624 === "1")` +5. Frontend передаёт флаг в форму подтверждения +6. Форма блокирует поля если `contact_data_confirmed = true` + +### Сценарий 2: Новое заявление (через n8n) +1. Пользователь вводит телефон +2. n8n вызывает `CreateWebContact` +3. `CreateWebContact` возвращает `cf_2624` в ответе +4. n8n извлекает `cf_2624` и передаёт в ответе для фронтенда +5. Frontend получает `contact_data_confirmed` из ответа n8n +6. Форма блокирует поля если `contact_data_confirmed = true` + +--- + +## 📋 Какие поля блокируются + +Если `contact_data_confirmed = true`, блокируются следующие поля: +- ✅ Фамилия (`lastname`) +- ✅ Имя (`firstname`) +- ✅ Отчество (`secondname`, `middle_name`) +- ✅ ИНН (`inn`) +- ✅ Дата рождения (`birthday`) +- ✅ Место рождения (`birthplace`, `birth_place`) +- ✅ Адрес (`mailingstreet`, `address`) +- ✅ Email (`email`) + +**Телефон (`mobile`) всегда только для чтения** (не зависит от флага) + +--- + +## 🧪 Проверка + +1. ✅ Создать контакт в CRM → `cf_2624` должен быть "0" +2. ✅ Загрузить черновик → поля должны быть редактируемыми +3. ⏳ Установить `cf_2624 = "1"` в CRM +4. ⏳ Загрузить черновик → поля должны быть заблокированы +5. ⏳ Проверить предупреждение "⚠️ Данные подтверждены" в форме + +--- + +## 📝 Документация + +- `ticket_form/docs/CRM_CONTACT_DATA_CONFIRMED_FIELD.md` - Описание поля cf_2624 +- `ticket_form/docs/CREATE_WEB_CONTACT_RESPONSE_FORMAT.md` - Формат ответа CreateWebContact +- `ticket_form/docs/N8N_UPDATE_CF_2624_IN_RESPONSE.md` - Обновление n8n workflow +- `ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js` - Код для n8n (обновлён) + diff --git a/ticket_form/docs/CF_2624_IN_OCR_STATUS_EVENT.md b/ticket_form/docs/CF_2624_IN_OCR_STATUS_EVENT.md new file mode 100644 index 00000000..f487aeac --- /dev/null +++ b/ticket_form/docs/CF_2624_IN_OCR_STATUS_EVENT.md @@ -0,0 +1,113 @@ +# Добавление cf_2624 в событие ocr_status ready + +## ✅ Да, правильно! + +Событие `ocr_status` с `status: "ready"` должно содержать поле `cf_2624` и сохраняться в черновик. + +## Формат события в Redis + +**Канал:** `ocr_events:sess_5fc7cdd1-a848-4e92-aed4-3ee4bfb19b4c` + +**Событие:** +```json +{ + "event_type": "ocr_status", + "status": "ready", + "claim_id": "ef853bac-f54b-46aa-adf8-f0c9c0cd76bc", + "message": "Заявление сформировано", + "timestamp": "2025-12-03T12:44:12.347Z", + "cf_2624": "0" +} +``` + +## Что происходит + +### 1. n8n workflow публикует событие + +После сохранения черновика (после `claimsave`) n8n публикует событие в Redis канал `ocr_events:{session_id}` с полем `cf_2624`. + +**Где добавить:** После ноды `claimsave`, перед публикацией в Redis. + +**См. подробности:** `ticket_form/docs/N8N_ADD_CF_2624_TO_OCR_STATUS_EVENT.md` + +--- + +### 2. Backend обрабатывает событие + +Backend получает событие из Redis и: +- ✅ Загружает `form_draft` из PostgreSQL +- ✅ **Сохраняет `cf_2624` в черновик** (в `payload.cf_2624`) +- ✅ Отправляет событие на фронтенд через SSE + +**Файл:** `ticket_form/backend/app/api/events.py` (строки 218-273) + +--- + +### 3. Сохранение в черновик + +`cf_2624` сохраняется в таблицу `clpr_claims` в поле `payload.cf_2624`: + +```sql +UPDATE clpr_claims +SET payload = jsonb_set( + COALESCE(payload, '{}'::jsonb), + '{cf_2624}', + '"0"'::jsonb -- или '"1"' +) +WHERE id::text = $1 OR payload->>'claim_id' = $1; +``` + +--- + +## Порядок работы + +1. **n8n workflow:** + - `CreateWebContacКлиентправ` → получает `cf_2624` из CRM + - `claimsave` → сохраняет черновик + - `Code: Prepare OCR Status Event` → формирует событие с `cf_2624` + - `HTTP Request` или `Redis Publish` → публикует в `ocr_events:{session_id}` + +2. **Backend:** + - Получает событие из Redis + - Сохраняет `cf_2624` в черновик + - Загружает `form_draft` из PostgreSQL + - Отправляет на фронтенд через SSE + +3. **Фронтенд:** + - Получает событие через SSE + - Использует `cf_2624` для блокировки полей + +--- + +## Проверка + +1. ✅ Событие публикуется в `ocr_events:{session_id}` с `cf_2624` +2. ✅ Backend сохраняет `cf_2624` в черновик (`payload.cf_2624`) +3. ✅ При загрузке черновика `cf_2624` доступен в `payload.cf_2624` + +--- + +## SQL для проверки + +```sql +-- Проверить, что cf_2624 сохранён в черновик +SELECT + id, + payload->>'claim_id' as claim_id, + payload->>'cf_2624' as cf_2624, + updated_at +FROM clpr_claims +WHERE payload->>'claim_id' = 'ef853bac-f54b-46aa-adf8-f0c9c0cd76bc' +ORDER BY updated_at DESC +LIMIT 1; +``` + +--- + +## Итого + +✅ **Да, правильно!** Событие `ocr_status` с `status: "ready"` должно содержать `cf_2624`, и это значение будет: +- Публиковаться в Redis канал `ocr_events:{session_id}` +- Сохраняться в черновик в `payload.cf_2624` +- Использоваться для блокировки полей на фронтенде + diff --git a/ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js b/ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js index b7c22a94..c536748d 100644 --- a/ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js +++ b/ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js @@ -1,7 +1,12 @@ // Парсим результат CreateWebContact const rawResult = $node["CreateWebContact"].json.result; -const contactData = JSON.parse(rawResult); // {"contact_id": "396625", "is_new": false} +const contactData = JSON.parse(rawResult); // {"contact_id": "396625", "is_new": false, "cf_2624": "1"} + +// ✅ Извлекаем cf_2624 (Данные подтверждены) +// "1" = данные подтверждены, "0" = не подтверждены +const cf_2624 = contactData.cf_2624 || "0"; +const contact_data_confirmed = cf_2624 === "1"; const phone = $('Edit Fields').first().json.phone; @@ -18,6 +23,8 @@ const sessionData = { contact_id: contactData.contact_id, // ← распарсенный ID из CreateWebContact phone: phone, is_new_contact: contactData.is_new, // ← флаг нового контакта + cf_2624: cf_2624, // ✅ Сохраняем cf_2624 в сессию + contact_data_confirmed: contact_data_confirmed, // ✅ Сохраняем флаг подтверждения status: "draft", current_step: 1, created_at: new Date().toISOString(), @@ -34,6 +41,10 @@ return { contact_id: contactData.contact_id, is_new_contact: contactData.is_new, phone: phone, + // ✅ Флаги подтверждения данных контакта (из cf_2624) + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: !contact_data_confirmed, redis_key: `session:${session_id}`, // ✅ Используем session_id для ключа Redis redis_value: JSON.stringify(sessionData), ttl: 604800 diff --git a/ticket_form/docs/CREATE_WEB_CONTACT_RESPONSE_FORMAT.md b/ticket_form/docs/CREATE_WEB_CONTACT_RESPONSE_FORMAT.md new file mode 100644 index 00000000..227f081b --- /dev/null +++ b/ticket_form/docs/CREATE_WEB_CONTACT_RESPONSE_FORMAT.md @@ -0,0 +1,56 @@ +# Формат ответа CreateWebContact + +## Обновление: добавлено поле cf_2624 + +### Старый формат: +```json +{ + "contact_id": "396625", + "is_new": false +} +``` + +### Новый формат (с cf_2624): +```json +{ + "contact_id": "396625", + "is_new": false, + "cf_2624": "1" +} +``` + +## Описание полей: + +- **contact_id** (string) - ID контакта в CRM +- **is_new** (boolean) - `true` если контакт только что создан, `false` если найден существующий +- **cf_2624** (string) - "Данные подтверждены": + - `"1"` = "Да" (данные подтверждены) + - `"0"` = "Нет" (данные не подтверждены) + +## Использование в n8n: + +```javascript +// Парсим результат CreateWebContact +const rawResult = $node["CreateWebContact"].json.result; +const contactData = JSON.parse(rawResult); + +// Получаем данные +const contact_id = contactData.contact_id; +const is_new = contactData.is_new; +const data_confirmed = contactData.cf_2624 === "1"; // true/false + +// Используем в дальнейшей логике +if (data_confirmed) { + // Данные подтверждены - блокируем редактирование +} +``` + +## Логика работы: + +1. **Новый контакт** (`is_new: true`): + - `cf_2624` всегда `"0"` (данные не подтверждены) + +2. **Существующий контакт** (`is_new: false`): + - `cf_2624` берётся из базы данных CRM + - Если поле пустое → возвращается `"0"` + diff --git a/ticket_form/docs/CRM_CONTACT_DATA_CONFIRMED_FIELD.md b/ticket_form/docs/CRM_CONTACT_DATA_CONFIRMED_FIELD.md new file mode 100644 index 00000000..981879ae --- /dev/null +++ b/ticket_form/docs/CRM_CONTACT_DATA_CONFIRMED_FIELD.md @@ -0,0 +1,149 @@ +# Добавление поля "Данные подтверждены" в CRM + +## Шаг 1: Создание кастомного поля в CRM + +1. Зайти в CRM → Настройки → Кастомные поля → Модуль "Контакты" +2. Создать новое поле: + - **Название:** "Данные подтверждены" + - **Тип:** "Да/Нет" (Checkbox) или "Список" (Picklist) со значениями "Да"/"Нет" + - **Код поля:** `cf_2624` ✅ (уже создано) + - **По умолчанию:** "Нет" (false) + +3. **ВАЖНО:** Записать номер поля (например, `cf_2624`) + +--- + +## Шаг 2: Обновление backend для проверки поля в CRM + +### Файл: `ticket_form/backend/app/api/claims.py` + +В функции `get_draft()` вместо проверки PostgreSQL, проверяем поле в CRM: + +```python +# ✅ Проверяем флаг подтверждения данных контакта из CRM +unified_id = row.get('unified_id') +contact_data_confirmed = False +contact_data_can_edit = True +contact_data_confirmed_at = None +contact_data_from_crm = None + +if unified_id: + # Получаем contact_id из payload + contact_id = payload.get('contact_id') if isinstance(payload, dict) else None + + if contact_id: + try: + # Получаем данные контакта из CRM + async with httpx.AsyncClient(timeout=30.0) as client: + # 1. Get Challenge + challenge_response = await client.get( + f"{settings.crm_webservice_url}", + params={"operation": "getchallenge", "username": "api"} + ) + challenge_data = challenge_response.json() + token = challenge_data.get("result", {}).get("token", "") + + # 2. Login + import hashlib + salt = "4r9ANex8PT2IuRV" + access_key = hashlib.md5((token + salt).encode()).hexdigest() + + login_response = await client.post( + f"{settings.crm_webservice_url}", + data={ + "operation": "login", + "username": "api", + "accessKey": access_key + } + ) + login_data = login_response.json() + session_name = login_data.get("result", {}).get("sessionName", "") + + # 3. Retrieve Contact + retrieve_response = await client.post( + f"{settings.crm_webservice_url}", + data={ + "operation": "retrieve", + "sessionName": session_name, + "id": f"12x{contact_id}" + } + ) + retrieve_data = retrieve_response.json() + + if retrieve_data.get("success") and retrieve_data.get("result"): + contact_data_from_crm = retrieve_data["result"] + + # ✅ Проверяем кастомное поле "Данные подтверждены" + confirmed_field = contact_data_from_crm.get("cf_2624", "0") # "1" = да, "0" = нет + contact_data_confirmed = confirmed_field == "1" or confirmed_field == "true" + contact_data_can_edit = not contact_data_confirmed + + logger.info( + f"🔒 Статус данных контакта из CRM: confirmed={contact_data_confirmed}, " + f"field_value={confirmed_field}" + ) + except Exception as e: + logger.warning(f"⚠️ Не удалось загрузить данные из CRM: {str(e)}") +``` + +--- + +## Шаг 3: Обновление n8n workflow для установки поля + +### В workflow `6mxRJ2LLHmQXyaDz` + +После подтверждения формы (после SMS-верификации) добавить ноду: + +**Название:** `HTTP Request: Set Contact Data Confirmed` + +**Метод:** POST + +**URL:** `{{ $env.CRM_WEBSERVICE_URL }}` + +**Body (form-data):** +``` +operation: revise +sessionName: {{ $('Login to CRM').json.sessionName }} +id: 12x{{ JSON.parse($node['CreateWebContacКлиентправ'].json.result).contact_id }} +cf_2624: 1 +``` + +**Где:** +- `cf_2624` - поле "Данные подтверждены" +- `1` = "Да" (данные подтверждены) + +--- + +## Шаг 4: Обновление UpsertContact (если используется) + +Если используется `UpsertContact.php`, добавить поддержку нового поля: + +```php +// В функции vtws_upsertcontact() +if (!empty($data_confirmed)) { + $params['cf_2624'] = $data_confirmed; // "1" или "0" +} +``` + +--- + +## Преимущества подхода: + +1. ✅ **CRM - источник истины** - все данные в одном месте +2. ✅ **Нет синхронизации** - не нужно синхронизировать флаги между PostgreSQL и CRM +3. ✅ **Простота** - один флаг в CRM, проверяем его напрямую +4. ✅ **Видимость** - менеджеры видят статус в карточке контакта +5. ✅ **Гибкость** - можно менять статус вручную в CRM + +--- + +## Проверка: + +1. ✅ Поле создано в CRM: `cf_2624` +2. ⏳ Обновить код backend (использовать `cf_2624`) +3. ⏳ Обновить n8n workflow (использовать `cf_2624`) +4. ⏳ Протестировать: + - Создать контакт → поле должно быть "Нет" + - Подтвердить форму → поле должно стать "Да" + - Загрузить черновик → поля должны быть заблокированы + diff --git a/ticket_form/docs/FRONTEND_UPDATE_CONTACT_DATA_CONFIRMED.md b/ticket_form/docs/FRONTEND_UPDATE_CONTACT_DATA_CONFIRMED.md new file mode 100644 index 00000000..67ca5d4f --- /dev/null +++ b/ticket_form/docs/FRONTEND_UPDATE_CONTACT_DATA_CONFIRMED.md @@ -0,0 +1,217 @@ +# Обновление фронтенда: Блокировка редактирования подтверждённых данных + +## Изменения + +### 1. Step1Phone.tsx - Получение флага из n8n + +**После получения ответа от n8n (после строки ~150):** + +```typescript +// ✅ Извлекаем флаг подтверждения данных +const contact_data_confirmed = result.contact_data_confirmed || false; +const contact_data_can_edit = result.contact_data_can_edit !== false; // По умолчанию true +const contact_data_confirmed_at = result.contact_data_confirmed_at || null; + +// Сохраняем в formData +updateFormData({ + // ... существующие поля ... + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: contact_data_can_edit, + contact_data_confirmed_at: contact_data_confirmed_at, +}); +``` + +--- + +### 2. generateConfirmationFormHTML.ts - Блокировка полей + +**Добавить параметр `contact_data_confirmed` в функцию:** + +```typescript +export function generateConfirmationFormHTML( + data: any, + contact_data_confirmed: boolean = false +): string { + // ... существующий код ... + + // В функции createInputField добавить проверку: + function createInputField(root: string, key: string, value: any, label: string, type: string = 'text') { + const isReadOnly = contact_data_confirmed && ( + key === 'firstname' || + key === 'lastname' || + key === 'middle_name' || + key === 'inn' || + key === 'birthday' || + key === 'birthplace' || + key === 'mailingstreet' || + key === 'email' + ); + + const readonlyAttr = isReadOnly ? 'readonly' : ''; + const readonlyClass = isReadOnly ? 'readonly-field' : ''; + + // ... остальной код с добавлением readonlyAttr и readonlyClass ... + } +} +``` + +**Добавить CSS для readonly полей:** + +```css +.readonly-field { + background-color: #f5f5f5 !important; + cursor: not-allowed !important; + opacity: 0.7; +} +``` + +--- + +### 3. StepClaimConfirmation.tsx - Передача флага в форму + +**В useEffect (после строки ~90):** + +```typescript +// Получаем флаг подтверждения из claimPlanData или formData +const contact_data_confirmed = + claimPlanData?.contact_data_confirmed || + claimPlanData?.propertyName?.meta?.contact_data_confirmed || + formData?.contact_data_confirmed || + false; + +// Передаём в generateConfirmationFormHTML +const html = generateConfirmationFormHTML(formData, contact_data_confirmed); +``` + +--- + +### 4. Добавить кнопку "Изменить данные" (опционально) + +**В generateConfirmationFormHTML.ts:** + +```typescript +// После заголовка формы, если contact_data_confirmed = true +if (contact_data_confirmed) { + html += ` +
+

+ ⚠️ Данные подтверждены +

+

+ Для изменения данных требуется подтверждение через SMS. +

+ +
+ `; +} +``` + +**В JavaScript внутри формы:** + +```javascript +// Обработчик кнопки "Изменить данные" +const editBtn = document.getElementById('btn-edit-data'); +if (editBtn) { + editBtn.addEventListener('click', function() { + // Отправляем сообщение родительскому окну + window.parent.postMessage({ + type: 'request_edit_contact_data', + eventData: { + phone: state.user?.mobile || '', + unified_id: state.meta?.unified_id || '' + } + }, '*'); + }); +} +``` + +--- + +### 5. Обработка запроса на изменение данных + +**В StepClaimConfirmation.tsx:** + +```typescript +useEffect(() => { + const handleMessage = (event: MessageEvent) => { + // ... существующие обработчики ... + + if (event.data.type === 'request_edit_contact_data') { + const { phone, unified_id } = event.data.eventData; + + // Показываем модалку SMS для подтверждения + setSmsModalVisible(true); + setSmsCodeSent(false); + sendSMSCode(phone); + + // Сохраняем флаг, что это запрос на изменение данных + setPendingFormData({ + ...pendingFormData, + is_edit_request: true, + unified_id: unified_id + }); + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); +}, []); +``` + +--- + +### 6. После SMS подтверждения - сброс флага + +**В verifySMSCode (после успешной верификации):** + +```typescript +// Если это запрос на изменение данных +if (pendingFormData?.is_edit_request) { + // Отправляем запрос в n8n для сброса флага + await fetch('/api/v1/claims/contact-data/reset-confirmed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + unified_id: pendingFormData.unified_id, + sms_code: code + }) + }); + + // Обновляем флаг в formData + updateFormData({ + contact_data_confirmed: false, + contact_data_can_edit: true + }); + + // Перезагружаем форму с разблокированными полями + // (можно просто обновить страницу или пересоздать форму) + window.location.reload(); +} +``` + +--- + +## Порядок реализации + +1. ✅ Обновить Step1Phone для получения флага +2. ✅ Обновить generateConfirmationFormHTML для блокировки полей +3. ✅ Обновить StepClaimConfirmation для передачи флага +4. ⏳ Добавить кнопку "Изменить данные" (опционально) +5. ⏳ Реализовать механизм переподтверждения через SMS + +--- + +## Тестирование + +После обновления проверить: +- ✅ Флаг получается из n8n +- ✅ Поля блокируются при `contact_data_confirmed = true` +- ✅ Данные из CRM загружаются и отображаются +- ✅ Кнопка "Изменить данные" работает (если реализована) + diff --git a/ticket_form/docs/N8N_ADD_CF_2624_TO_OCR_STATUS_EVENT.md b/ticket_form/docs/N8N_ADD_CF_2624_TO_OCR_STATUS_EVENT.md new file mode 100644 index 00000000..28b449b0 --- /dev/null +++ b/ticket_form/docs/N8N_ADD_CF_2624_TO_OCR_STATUS_EVENT.md @@ -0,0 +1,209 @@ +# Добавление cf_2624 в событие ocr_status ready + +## Задача + +После сохранения черновика (после `claimsave`) публиковать событие `ocr_status` с `status: "ready"` в Redis канал `ocr_events:{session_id}` с полем `cf_2624`. + +## Формат события + +```json +{ + "event_type": "ocr_status", + "status": "ready", + "claim_id": "ef853bac-f54b-46aa-adf8-f0c9c0cd76bc", + "message": "Заявление сформировано", + "timestamp": "2025-12-03T12:44:12.347Z", + "cf_2624": "0" +} +``` + +## Где добавить в n8n workflow + +### Вариант 1: После ноды `claimsave` (PostgreSQL) + +**Название ноды:** `Code: Prepare OCR Status Event` + +**Расположение:** После ноды `claimsave` (PostgreSQL), перед нодой публикации в Redis + +**Код:** +```javascript +// Получаем результат из claimsave +const claimResult = $input.first().json; +const claim = claimResult.claim || claimResult; + +// Получаем contact_id из claim +const contact_id = claim.contact_id; + +// ✅ Получаем cf_2624 из PostgreSQL (если есть нода Get Contact Data) +let cf_2624 = "0"; // По умолчанию "0" (не подтверждено) + +try { + // Пытаемся получить из предыдущей ноды PostgreSQL: Get Contact Data + const contactData = $('PostgreSQL: Get Contact Data')?.first()?.json; + if (contactData && contactData.cf_2624) { + cf_2624 = contactData.cf_2624; + } else { + // Альтернатива: получаем из CreateWebContact + const createWebContactResult = $node["CreateWebContacКлиентправ"]?.json?.result || ""; + if (createWebContactResult) { + const contactData = typeof createWebContactResult === 'string' + ? JSON.parse(createWebContactResult) + : createWebContactResult; + if (contactData.cf_2624) { + cf_2624 = contactData.cf_2624; + } + } + } +} catch (e) { + console.warn('⚠️ Не удалось получить cf_2624, используем значение по умолчанию "0"'); +} + +// Формируем событие для Redis +const event = { + event_type: 'ocr_status', + status: 'ready', + claim_id: claim.claim_id || claim.id, + message: 'Заявление сформировано', + timestamp: new Date().toISOString(), + cf_2624: cf_2624 // ✅ Добавляем cf_2624 +}; + +console.log('📤 Подготовлено событие ocr_status ready:', { + claim_id: event.claim_id, + cf_2624: event.cf_2624, + contact_id: contact_id +}); + +return { + json: { + // Данные для публикации в Redis + channel: `ocr_events:${claim.session_token || claim.session_id}`, + message: JSON.stringify(event), + + // Передаём дальше для следующих нод + claim_id: event.claim_id, + session_token: claim.session_token || claim.session_id, + cf_2624: cf_2624 + } +}; +``` + +--- + +### Вариант 2: Прямо в ноде публикации (HTTP Request или Redis Publish) + +**Если используется HTTP Request:** + +**URL:** `{{ $env.BACKEND_URL }}/api/v1/events/{{ $json.session_token }}` + +**Body (JSON):** +```json +{ + "event_type": "ocr_status", + "status": "ready", + "message": "Заявление сформировано", + "data": { + "claim_id": "{{ $json.claim_id }}", + "cf_2624": "{{ $json.cf_2624 || '0' }}" + }, + "timestamp": "{{ $now.toISO() }}" +} +``` + +**Если используется Redis Publish:** + +**Channel:** `ocr_events:{{ $json.session_token }}` + +**Message:** +```javascript +={{ JSON.stringify({ + event_type: 'ocr_status', + status: 'ready', + claim_id: $json.claim_id, + message: 'Заявление сформировано', + timestamp: new Date().toISOString(), + cf_2624: $json.cf_2624 || '0' +}) }} +``` + +--- + +## Порядок нод в workflow + +1. **CreateWebContacКлиентправ** → получаем `contact_id` и `cf_2624` +2. **PostgreSQL: Get Contact Data** (опционально) → получаем полные данные контакта включая `cf_2624` +3. **claimsave** (PostgreSQL) → сохраняем черновик +4. **Code: Prepare OCR Status Event** → формируем событие с `cf_2624` +5. **HTTP Request** или **Redis Publish** → публикуем событие в `ocr_events:{session_id}` + +--- + +## Сохранение в черновик + +Событие с `cf_2624` будет: +1. ✅ Публиковаться в Redis канал `ocr_events:{session_id}` +2. ✅ Обрабатываться backend'ом (загружает `form_draft` из PostgreSQL) +3. ⏳ **Нужно добавить:** Сохранение `cf_2624` в черновик при обработке события + +### Обновление backend для сохранения cf_2624 + +В файле `ticket_form/backend/app/api/events.py` (строка 218-267): + +После загрузки `form_draft` из PostgreSQL, если в событии есть `cf_2624`, нужно сохранить его в черновик: + +```python +# ✅ Обработка ocr_status ready: загружаем form_draft из PostgreSQL +if actual_event.get('event_type') == 'ocr_status' and actual_event.get('status') == 'ready': + claim_id = actual_event.get('claim_id') or actual_event.get('data', {}).get('claim_id') + cf_2624 = actual_event.get('cf_2624') # ✅ Получаем cf_2624 из события + + if claim_id: + # ... существующий код загрузки form_draft ... + + # ✅ Если есть cf_2624 в событии - сохраняем в черновик + if cf_2624: + try: + update_query = """ + UPDATE clpr_claims + SET payload = jsonb_set( + payload, + '{cf_2624}', + $1::jsonb + ) + WHERE id::text = $2 + RETURNING id; + """ + await db.execute(update_query, json.dumps(cf_2624), claim_id) + logger.info(f"✅ Сохранён cf_2624={cf_2624} в черновик claim_id={claim_id}") + except Exception as e: + logger.warning(f"⚠️ Не удалось сохранить cf_2624: {e}") +``` + +--- + +## Проверка + +1. ✅ Событие публикуется в `ocr_events:{session_id}` с `cf_2624` +2. ⏳ Backend обрабатывает событие и сохраняет `cf_2624` в черновик +3. ⏳ При загрузке черновика `cf_2624` доступен в `payload.cf_2624` + +--- + +## Пример полного события + +```json +{ + "event_type": "ocr_status", + "status": "ready", + "claim_id": "ef853bac-f54b-46aa-adf8-f0c9c0cd76bc", + "message": "Заявление сформировано", + "timestamp": "2025-12-03T12:44:12.347Z", + "cf_2624": "0" +} +``` + +Это событие будет: +- ✅ Публиковаться в Redis канал `ocr_events:sess_5fc7cdd1-a848-4e92-aed4-3ee4bfb19b4c` +- ✅ Обрабатываться backend'ом +- ✅ Сохраняться в черновик в поле `payload.cf_2624` + diff --git a/ticket_form/docs/N8N_CODE_CHECK_CONTACT_DATA_CONFIRMED.js b/ticket_form/docs/N8N_CODE_CHECK_CONTACT_DATA_CONFIRMED.js new file mode 100644 index 00000000..bec24e88 --- /dev/null +++ b/ticket_form/docs/N8N_CODE_CHECK_CONTACT_DATA_CONFIRMED.js @@ -0,0 +1,44 @@ +// ============================================================================ +// Code Node для n8n: Проверка подтверждения данных контакта +// ============================================================================ +// Назначение: Проверить, подтверждены ли данные контакта пользователя +// и нужно ли блокировать редактирование +// +// Использование: После получения unified_id, перед загрузкой данных формы +// ============================================================================ + +// Получаем unified_id из предыдущих шагов +const unified_id = $('user_get').first().json.unified_id || + $('Edit Fields').first().json.unified_id || + $json.unified_id; + +if (!unified_id) { + throw new Error('unified_id не найден'); +} + +// Выполняем SQL запрос для проверки статуса +// (это должно быть в PostgreSQL ноде, но для примера показываю логику) + +// SQL запрос: +// SELECT * FROM clpr_get_contact_data_status($1); +// Параметр: unified_id + +// Ожидаемый результат: +// { +// is_confirmed: true/false, +// confirmed_at: "2025-12-02T14:30:00Z" или null, +// can_edit: true/false +// } + +// Для Code Node (если нужно обработать результат): +const status = $('PostgreSQL Check Status').first().json; // Предполагаем, что есть такая нода + +return { + unified_id: unified_id, + is_confirmed: status.is_confirmed || false, + confirmed_at: status.confirmed_at || null, + can_edit: status.can_edit !== false, // По умолчанию можно редактировать + // Флаг для фронтенда + lock_editing: status.is_confirmed || false +}; + diff --git a/ticket_form/docs/N8N_CODE_IN_JAVASCRIPT_КЛИЕНТПРАВ_FULL.js b/ticket_form/docs/N8N_CODE_IN_JAVASCRIPT_КЛИЕНТПРАВ_FULL.js new file mode 100644 index 00000000..87fab60d --- /dev/null +++ b/ticket_form/docs/N8N_CODE_IN_JAVASCRIPT_КЛИЕНТПРАВ_FULL.js @@ -0,0 +1,264 @@ +// ======================================== +// Code Node: Code in JavaScriptКлиентправ +// Формирование Response для фронтенда с поддержкой cf_2624 +// ======================================== + +// --- 1. Генерация UUIDv4 --- +function generateUUIDv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : ((r & 0x3) | 0x8); + return v.toString(16); + }); +} + +// --- 2. Парсим контакт из CreateWebContacКлиентправ --- +const createWebContactNode = $node["CreateWebContacКлиентправ"] || $node["CreateWebContact"]; +const rawResult = createWebContactNode?.json?.result || ""; + +let contactData = {}; +try { + contactData = typeof rawResult === 'string' + ? JSON.parse(rawResult) + : rawResult; +} catch (e) { + console.error('❌ Ошибка парсинга CreateWebContact:', e); + contactData = {}; +} + +// ✅ Извлекаем cf_2624 (Данные подтверждены) из CreateWebContact +// "1" = данные подтверждены, "0" = не подтверждены +const cf_2624 = contactData.cf_2624 || "0"; +const contact_data_confirmed = cf_2624 === "1" || cf_2624 === "true" || cf_2624 === true; +const contact_data_can_edit = !contact_data_confirmed; + +console.log('🔒 Статус данных контакта из CreateWebContact:', { + contact_id: contactData.contact_id, + is_new: contactData.is_new, + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: contact_data_can_edit +}); + +// --- 2.1. Получаем полные данные контакта из PostgreSQL (если есть) --- +let contactFromDB = null; +try { + // Пытаемся найти ноду PostgreSQL, которая получила данные контакта + const possiblePostgresNodes = [ + 'PostgreSQL: Get Contact Data', + 'Get Contact from DB', + 'PostgreSQL', + 'Get Contact Details' + ]; + + for (const nodeName of possiblePostgresNodes) { + try { + const node = $(nodeName)?.first(); + if (node && node.json) { + // Проверяем, что это данные контакта (есть contactid) + if (node.json.contactid || node.json.contact_id) { + contactFromDB = node.json; + console.log('✅ Получены данные контакта из PostgreSQL:', { + contactid: contactFromDB.contactid || contactFromDB.contact_id, + firstname: contactFromDB.firstname, + lastname: contactFromDB.lastname + }); + break; + } + } + } catch (e) { + continue; + } + } + + // Альтернативный способ: ищем по структуре данных + if (!contactFromDB) { + // Может быть в предыдущей ноде с результатом запроса + const inputData = $input.all(); + for (const item of inputData) { + if (item.json && (item.json.contactid || item.json.contact_id)) { + contactFromDB = item.json; + break; + } + } + } +} catch (e) { + console.warn('⚠️ Не удалось получить данные контакта из PostgreSQL:', e.message); +} + +// Если данные из БД получены - используем их для дополнения информации +if (contactFromDB) { + console.log('📋 Данные контакта из БД:', { + contactid: contactFromDB.contactid, + firstname: contactFromDB.firstname, + lastname: contactFromDB.lastname, + email: contactFromDB.email, + mobile: contactFromDB.mobile, + birthday: contactFromDB.birthday, + mailingstreet: contactFromDB.mailingstreet, + middle_name: contactFromDB.middle_name, + birthplace: contactFromDB.birthplace, + inn: contactFromDB.inn + }); +} + +// --- 3. Телефон из Edit Fields --- +let phone = null; +try { + const editFields = $('Edit Fields')?.first(); + if (editFields && editFields.json) { + phone = editFields.json.phone; + } +} catch (e) { + console.warn('⚠️ Не удалось получить phone из Edit Fields:', e.message); +} + +// --- 4. unified_id из user_get --- +let unified_id = null; +try { + const possibleUserNodes = ['user_get', 'Find or Create User', 'PostgreSQL: Find User']; + for (const nodeName of possibleUserNodes) { + try { + const node = $node[nodeName]; + if (node && node.json && node.json.unified_id) { + unified_id = node.json.unified_id; + break; + } + } catch (e) { + // Нода не существует или не выполнена - продолжаем поиск + continue; + } + } + + if (!unified_id) { + console.warn('⚠️ unified_id не получен из ноды user_get. Проверьте, что нода выполнена.'); + } +} catch (e) { + console.warn('⚠️ Не удалось получить unified_id:', e.message); +} + +// --- 5. Генерируем session_id (если не получен из предыдущих нод) --- +let session_id = null; + +// Пытаемся получить session_id из предыдущих нод +try { + const possibleSessionNodes = [ + 'Code in JavaScript1', + 'Code in JavaScript', + 'Set Session Data', + 'Create Session' + ]; + + for (const nodeName of possibleSessionNodes) { + try { + const node = $(nodeName)?.first(); + if (node && node.json) { + if (node.json.session_id) { + session_id = node.json.session_id; + break; + } else if (node.json.redis_value) { + const parsed = JSON.parse(node.json.redis_value); + if (parsed.session_id) { + session_id = parsed.session_id; + break; + } + } + } + } catch (e) { + continue; + } + } + + // Пытаемся получить из Edit Fields + if (!session_id) { + try { + const editFields = $('Edit Fields')?.first(); + if (editFields && editFields.json && editFields.json.session_id) { + session_id = editFields.json.session_id; + } + } catch (e) { + // Игнорируем + } + } +} catch (e) { + console.warn('⚠️ Не удалось получить session_id из предыдущих нод:', e.message); +} + +// Если session_id не найден - генерируем новый +if (!session_id) { + session_id = 'sess_' + generateUUIDv4(); + console.log('✅ Сгенерирован новый session_id:', session_id); +} + +// --- 6. Формируем sessionData для Redis --- +const sessionData = { + session_id, // ← теперь сохраняем внутрь + unified_id, + contact_id: contactData.contact_id, + phone, + is_new_contact: contactData.is_new || contactData.is_new_contact || false, + // ✅ Флаги подтверждения данных контакта (из cf_2624) + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: contact_data_can_edit, + // ✅ Данные контакта из PostgreSQL (если получены) + contact_from_db: contactFromDB ? { + contactid: contactFromDB.contactid || contactFromDB.contact_id, + firstname: contactFromDB.firstname, + lastname: contactFromDB.lastname, + email: contactFromDB.email, + mobile: contactFromDB.mobile, + phone: contactFromDB.phone, + birthday: contactFromDB.birthday, + mailingstreet: contactFromDB.mailingstreet, + mailingcity: contactFromDB.mailingcity, + mailingstate: contactFromDB.mailingstate, + mailingzip: contactFromDB.mailingzip, + mailingcountry: contactFromDB.mailingcountry, + middle_name: contactFromDB.middle_name, + birthplace: contactFromDB.birthplace, + inn: contactFromDB.inn, + requisites: contactFromDB.requisites, + code: contactFromDB.code, + sms: contactFromDB.sms + } : null, + status: "draft", + current_step: 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + documents: {}, + email: contactFromDB?.email || null, + bank_name: null +}; + +// --- 7. Возвращаем результат в формате items --- +const result = { + json: { + session: session_id, + session_id, + unified_id, + contact_id: contactData.contact_id, + is_new_contact: contactData.is_new || contactData.is_new_contact || false, + phone, + // ✅ Флаги подтверждения данных контакта (из cf_2624) + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: contact_data_can_edit, + redis_key: `session:${session_id}`, + redis_value: JSON.stringify(sessionData), + ttl: 604800 + } +}; + +// Логируем финальный ответ для отладки +console.log('✅ Сформирован ответ для фронтенда:', { + session_id: result.json.session_id, + has_unified_id: !!result.json.unified_id, + has_contact_id: !!result.json.contact_id, + contact_data_confirmed: result.json.contact_data_confirmed, + cf_2624: result.json.cf_2624, + is_new_contact: result.json.is_new_contact +}); + +return [result]; + diff --git a/ticket_form/docs/N8N_CODE_SET_CONTACT_DATA_CONFIRMED.js b/ticket_form/docs/N8N_CODE_SET_CONTACT_DATA_CONFIRMED.js new file mode 100644 index 00000000..3911a43f --- /dev/null +++ b/ticket_form/docs/N8N_CODE_SET_CONTACT_DATA_CONFIRMED.js @@ -0,0 +1,51 @@ +// ============================================================================ +// Code Node для n8n: Установка флага подтверждения данных +// ============================================================================ +// Назначение: Установить флаг contact_data_confirmed_at после подтверждения формы +// +// Использование: После успешного сохранения данных в CRM через claim_confirmed +// ============================================================================ + +// Получаем unified_id +const unified_id = $('user_get').first().json.unified_id || + $json.unified_id; + +if (!unified_id) { + throw new Error('unified_id не найден для установки флага подтверждения'); +} + +// Получаем contact_id из CRM (если есть) +const contact_id = $node['CreateWebContacКлиентправ']?.json?.result?.contact_id || + $json.contact_id || + null; + +// Проверяем, есть ли данные в CRM (для автоматического подтверждения) +// Если contact_id > 0, значит данные уже есть в CRM - подтверждаем автоматически +const has_crm_data = contact_id && parseInt(contact_id) > 0; + +// Формируем данные для PostgreSQL +return { + unified_id: unified_id, + contact_id: contact_id, + has_crm_data: has_crm_data, + // Флаг для SQL функции + should_confirm: true, // Всегда подтверждаем после сохранения формы + confirmed_at: new Date().toISOString() +}; + +// ============================================================================ +// SQL запрос для PostgreSQL ноды (после этого Code Node): +// ============================================================================ +// SELECT clpr_set_contact_data_confirmed($1, $2::timestamptz); +// +// Параметры: +// $1 = {{ $json.unified_id }} +// $2 = {{ $json.confirmed_at }} +// +// ИЛИ для автоматического подтверждения существующих данных: +// SELECT clpr_auto_confirm_if_crm_has_data($1, $2::integer); +// +// Параметры: +// $1 = {{ $json.unified_id }} +// $2 = {{ $json.contact_id }} + diff --git a/ticket_form/docs/N8N_MYSQL_GET_CONTACT_DATA.md b/ticket_form/docs/N8N_MYSQL_GET_CONTACT_DATA.md new file mode 100644 index 00000000..18f878a4 --- /dev/null +++ b/ticket_form/docs/N8N_MYSQL_GET_CONTACT_DATA.md @@ -0,0 +1,73 @@ +# Получение данных контакта из MySQL в n8n + +## Задача + +В n8n workflow нужно получить полные данные контакта из MySQL БД vtiger CRM перед формированием финального ответа. + +## SQL запрос + +**Файл:** `ticket_form/docs/N8N_POSTGRESQL_GET_CONTACT_DATA.sql` (название файла устарело, но запрос для MySQL) + +```sql +SELECT + cd.contactid, + cd.firstname, + cd.lastname, + cd.email, + cd.mobile, + cd.phone, + cs.birthday, + ca.mailingstreet, + ca.mailingcity, + ca.mailingstate, + ca.mailingzip, + ca.mailingcountry, + ccf.cf_1157 AS middle_name, + ccf.cf_1263 AS birthplace, + ccf.cf_1257 AS inn, + ccf.cf_1849 AS requisites, + ccf.cf_1580 AS code, + ccf.cf_1706 AS sms, + ccf.cf_2624 AS cf_2624 +FROM vtiger_contactdetails cd +LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid +LEFT JOIN vtiger_contactsubdetails cs ON cs.contactsubscriptionid = cd.contactid +LEFT JOIN vtiger_contactaddress ca ON ca.contactaddressid = cd.contactid +LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid +WHERE cd.contactid = ? + AND ce.deleted = 0 +LIMIT 1; +``` + +## Настройка ноды MySQL в n8n + +1. **Тип ноды:** MySQL +2. **Operation:** Execute Query +3. **Query:** (см. выше) +4. **Parameters:** + - `?` = `{{ JSON.parse($node["CreateWebContacКлиентправ"].json.result).contact_id }}` + +## Credentials для MySQL + +- **Host:** `localhost` +- **Port:** `3306` +- **Database:** `ci20465_72new` +- **User:** `ci20465_72new` +- **Password:** `EcY979Rn` + +## Использование в Code node + +После выполнения MySQL запроса, данные доступны в Code node: + +```javascript +const pgContactNode = $('MySQL: Get Contact Data')?.first(); +if (pgContactNode && pgContactNode.json && pgContactNode.json.length > 0) { + const contactFromDb = pgContactNode.json[0]; + // Используем contactFromDb.cf_2624, contactFromDb.firstname, и т.д. +} +``` + +--- + +**Примечание:** Название файла `N8N_POSTGRESQL_GET_CONTACT_DATA.sql` устарело, но запрос работает для MySQL. + diff --git a/ticket_form/docs/N8N_SET_CF_2624_CONTACT_CONFIRMED.md b/ticket_form/docs/N8N_SET_CF_2624_CONTACT_CONFIRMED.md new file mode 100644 index 00000000..c743671e --- /dev/null +++ b/ticket_form/docs/N8N_SET_CF_2624_CONTACT_CONFIRMED.md @@ -0,0 +1,62 @@ +# Установка поля cf_2624 "Данные подтверждены" в n8n workflow + +## Обновление workflow 6mxRJ2LLHmQXyaDz + +### После подтверждения формы (после SMS-верификации) + +**Добавить ноду:** `HTTP Request: Set Contact Data Confirmed` + +**Параметры:** +- **Method:** POST +- **URL:** `{{ $env.CRM_WEBSERVICE_URL }}` (или полный URL CRM webservice) +- **Body Type:** form-data + +**Body (form-data):** +``` +operation: revise +sessionName: {{ $('Login to CRM').json.sessionName }} +id: 12x{{ JSON.parse($node['CreateWebContacКлиентправ'].json.result).contact_id }} +cf_2624: 1 +``` + +**Где:** +- `cf_2624` - поле "Данные подтверждены" в CRM +- `1` = "Да" (данные подтверждены) +- `0` = "Нет" (данные не подтверждены) + +--- + +## Альтернативный вариант: через Code Node + +Если нужно более гибкое управление, можно использовать Code Node: + +**Название:** `Code: Set Contact Data Confirmed` + +**Код:** +```javascript +// Получаем contact_id из CreateWebContact +const contactResult = JSON.parse($node['CreateWebContacКлиентправ'].json.result); +const contact_id = contactResult.contact_id; + +// Получаем sessionName из Login to CRM +const sessionName = $('Login to CRM').json.sessionName; + +// Формируем данные для обновления +return { + operation: 'revise', + sessionName: sessionName, + id: `12x${contact_id}`, + cf_2624: '1' // Устанавливаем "Да" (данные подтверждены) +}; +``` + +Затем подключить к **HTTP Request** ноде, которая отправит эти данные в CRM. + +--- + +## Проверка работы: + +1. После SMS-верификации и подтверждения формы +2. Проверить в CRM, что у контакта поле `cf_2624` = "Да" +3. При следующей загрузке черновика поля должны быть заблокированы + diff --git a/ticket_form/docs/N8N_UPDATE_CF_2624_IN_RESPONSE.md b/ticket_form/docs/N8N_UPDATE_CF_2624_IN_RESPONSE.md new file mode 100644 index 00000000..916fb27b --- /dev/null +++ b/ticket_form/docs/N8N_UPDATE_CF_2624_IN_RESPONSE.md @@ -0,0 +1,146 @@ +# Обновление n8n workflow: Использование cf_2624 из CreateWebContact + +## Задача + +При формировании заявления проверять значение `cf_2624` из ответа `CreateWebContact`: +- Если `cf_2624 = "0"` → данные можно редактировать +- Если `cf_2624 = "1"` → данные только для просмотра (readonly) + +## Изменения в workflow 6mxRJ2LLHmQXyaDz + +### 1. После ноды `CreateWebContacКлиентправ` + +**Название ноды:** `Code: Extract Contact Data Confirmed` + +**Код:** +```javascript +// Парсим результат CreateWebContact +const rawResult = $node["CreateWebContacКлиентправ"].json.result; +const contactData = JSON.parse(rawResult); + +// Извлекаем cf_2624 (Данные подтверждены) +// "1" = данные подтверждены, "0" = не подтверждены +const cf_2624 = contactData.cf_2624 || "0"; +const contact_data_confirmed = cf_2624 === "1"; + +console.log('🔒 Статус данных контакта:', { + contact_id: contactData.contact_id, + is_new: contactData.is_new, + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed +}); + +return { + contact_id: contactData.contact_id, + is_new_contact: contactData.is_new, + cf_2624: cf_2624, + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: !contact_data_confirmed +}; +``` + +--- + +### 2. В ноде `Code in JavaScriptКлиентправ` (формирование ответа для фронтенда) + +**Добавить в return:** + +```javascript +// Получаем данные о подтверждении из предыдущей ноды +const contactStatus = $('Code: Extract Contact Data Confirmed').first().json; + +return { + // ... существующие поля ... + session: session_id, + session_id: session_id, + unified_id: unified_id, + contact_id: contactStatus.contact_id, + is_new_contact: contactStatus.is_new_contact, + + // ✅ Флаги подтверждения данных контакта (из cf_2624) + contact_data_confirmed: contactStatus.contact_data_confirmed || false, + contact_data_can_edit: contactStatus.contact_data_can_edit !== false, + cf_2624: contactStatus.cf_2624 || "0", + + // ... остальные поля ... +}; +``` + +--- + +### 3. При загрузке черновика (если используется отдельный workflow) + +**Если есть нода для загрузки черновика:** + +```javascript +// Получаем contact_id из черновика +const contact_id = $json.contact_id || $json.payload?.contact_id; + +if (contact_id) { + // Вызываем CreateWebContact для получения cf_2624 + // (или используем retrieve из CRM) + + // Для простоты можно использовать retrieve: + const retrieveResult = await $http.post('{{ $env.CRM_WEBSERVICE_URL }}', { + operation: 'retrieve', + sessionName: $('Login to CRM').json.sessionName, + id: `12x${contact_id}` + }); + + const cf_2624 = retrieveResult.result?.cf_2624 || "0"; + const contact_data_confirmed = cf_2624 === "1"; + + return { + // ... данные черновика ... + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: !contact_data_confirmed, + cf_2624: cf_2624 + }; +} +``` + +--- + +## Логика работы + +1. **При создании/поиске контакта:** + - `CreateWebContact` возвращает `cf_2624` в ответе + - Извлекаем значение и передаём в ответе для фронтенда + +2. **При загрузке черновика:** + - Backend API `/drafts/{claim_id}` уже получает `cf_2624` из CRM + - Фронтенд получает `contact_data_confirmed` из ответа API + - Передаёт в `StepClaimConfirmation` → `generateConfirmationFormHTML` + +3. **При формировании заявления:** + - Если `cf_2624 = "1"` → поля персональных данных блокируются (readonly) + - Если `cf_2624 = "0"` → поля можно редактировать + +--- + +## Проверка + +1. ✅ `CreateWebContact` возвращает `cf_2624` в ответе +2. ⏳ n8n workflow извлекает `cf_2624` и передаёт в ответе +3. ⏳ Фронтенд получает `contact_data_confirmed` и блокирует поля +4. ⏳ Backend API `/drafts/{claim_id}` получает `cf_2624` из CRM + +--- + +## Пример ответа от n8n: + +```json +{ + "success": true, + "result": { + "session": "sess_...", + "contact_id": "399542", + "unified_id": "usr_...", + "contact_data_confirmed": true, + "contact_data_can_edit": false, + "cf_2624": "1", + "is_new_contact": false + } +} +``` + diff --git a/ticket_form/docs/N8N_WORKFLOW_6mxRJ2LLHmQXyaDz_CHANGES.md b/ticket_form/docs/N8N_WORKFLOW_6mxRJ2LLHmQXyaDz_CHANGES.md new file mode 100644 index 00000000..e91c8bc4 --- /dev/null +++ b/ticket_form/docs/N8N_WORKFLOW_6mxRJ2LLHmQXyaDz_CHANGES.md @@ -0,0 +1,135 @@ +# Конкретные изменения в workflow 6mxRJ2LLHmQXyaDz + +## Что менять: + +### 1. После ноды `user_get` → добавить PostgreSQL ноду (ПЕРВАЯ) + +**Название ноды:** `PostgreSQL: Auto Confirm Contact Data` + +**Параметры:** +- **Operation:** Execute Query +- **Query:** +```sql +SELECT clpr_auto_confirm_if_crm_has_data($1, $2::integer); +``` +- **Parameters:** + - `$1` = `{{ $json.unified_id }}` ← используем данные из предыдущей ноды (user_get) + - `$2` = `{{ JSON.parse($node['CreateWebContacКлиентправ'].json.result).contact_id }}` + +**Подключение:** +- `user_get` → `PostgreSQL: Auto Confirm Contact Data` → `Execute a SQL query2` + +--- + +### 2. После ноды `PostgreSQL: Auto Confirm Contact Data` → добавить PostgreSQL ноду (ВТОРАЯ) + +**Название ноды:** `PostgreSQL: Check Contact Data Status` + +**Параметры:** +- **Operation:** Execute Query +- **Query:** +```sql +SELECT * FROM clpr_get_contact_data_status($1); +``` +- **Parameters:** + - `$1` = `{{ $json.unified_id }}` ← unified_id передаётся дальше по цепочке + +**Подключение:** +- `PostgreSQL: Auto Confirm Contact Data` → `PostgreSQL: Check Contact Data Status` → `Execute a SQL query2` + +--- + +### 3. В ноде `Code in JavaScript` (та что перед `Respond to Webhook1`) → добавить флаг в ответ + +**Найти эту строку:** +```javascript +// Unified ID из PostgreSQL (обязательно!) +unified_id: userData.unified_id, // из ноды user_get (PostgreSQL: Find or Create User) +``` + +**Добавить ПОСЛЕ неё:** +```javascript +// Флаг подтверждения данных контакта +contact_data_confirmed: $('PostgreSQL: Check Contact Data Status').first().json.is_confirmed || false, +contact_data_can_edit: $('PostgreSQL: Check Contact Data Status').first().json.can_edit !== false, +contact_data_confirmed_at: $('PostgreSQL: Check Contact Data Status').first().json.confirmed_at || null, +``` + +**Полный return должен быть:** +```javascript +return { + success: true, + result: { + session: $('Code in JavaScript3').first().json.session_id, + contact_id: sessionData.contact_id || claimResult.contact_id, + project_id: sessionData.project_id, + + // Unified ID из PostgreSQL (обязательно!) + unified_id: userData.unified_id, + + // Флаг подтверждения данных контакта + contact_data_confirmed: $('PostgreSQL: Check Contact Data Status').first().json.is_confirmed || false, + contact_data_can_edit: $('PostgreSQL: Check Contact Data Status').first().json.can_edit !== false, + contact_data_confirmed_at: $('PostgreSQL: Check Contact Data Status').first().json.confirmed_at || null, + + // Данные заявки + ticket_id: claimResult.ticket_id, + ticket_number: claimResult.ticket_number, + title: claimResult.title, + category: claimResult.category, + status: claimResult.status, + + // Метаданные + event_type: sessionData.event_type, + current_step: sessionData.current_step || 1, + updated_at: sessionData.updated_at || new Date().toISOString(), + + // Дополнительно + is_new_contact: claimResult.is_new_contact || false + } +}; +``` + +--- + +## Итого: 3 изменения + +1. ✅ Добавить ноду `PostgreSQL: Auto Confirm Contact Data` после `CreateWebContacКлиентправ` +2. ✅ Добавить ноду `PostgreSQL: Check Contact Data Status` после `user_get` +3. ✅ Добавить 3 строки в `Code in JavaScript` перед `Respond to Webhook1` + +--- + +## Порядок выполнения в workflow: + +``` +contact → Edit Fields → Get Challenge → ... → Login to CRM → form_id + ↓ + CreateWebContacКлиентправ + ↓ + [НОВАЯ] PostgreSQL: Auto Confirm Contact Data + ↓ + Code in JavaScriptКлиентправ + ↓ + user_get + ↓ + [НОВАЯ] PostgreSQL: Check Contact Data Status + ↓ + Execute a SQL query2 + ↓ + ... + ↓ + Code in JavaScript (← ДОБАВИТЬ ФЛАГИ) + ↓ + Respond to Webhook1 +``` + +--- + +## Проверка: + +После изменений в ответе n8n должны быть поля: +- `contact_data_confirmed` (true/false) +- `contact_data_can_edit` (true/false) +- `contact_data_confirmed_at` (дата или null) + diff --git a/ticket_form/docs/N8N_WORKFLOW_ADD_POSTGRESQL_CONTACT.md b/ticket_form/docs/N8N_WORKFLOW_ADD_POSTGRESQL_CONTACT.md new file mode 100644 index 00000000..49685e98 --- /dev/null +++ b/ticket_form/docs/N8N_WORKFLOW_ADD_POSTGRESQL_CONTACT.md @@ -0,0 +1,99 @@ +# Добавление ноды PostgreSQL для получения данных контакта + +## Задача + +Добавить ноду PostgreSQL перед "Code in JavaScriptКлиентправ" для получения полных данных контакта из CRM. + +## Шаги + +### 1. Добавить ноду PostgreSQL + +**Название ноды:** `PostgreSQL: Get Contact Data` + +**Параметры:** +- **Operation:** Execute Query +- **Query:** (см. файл `N8N_POSTGRESQL_GET_CONTACT_DATA.sql`) + +**SQL запрос:** +```sql +SELECT + cd.contactid, + cd.firstname, + cd.lastname, + cd.email, + cd.mobile, + cd.phone, + cs.birthday, + ca.mailingstreet, + ca.mailingcity, + ca.mailingstate, + ca.mailingzip, + ca.mailingcountry, + ccf.cf_1157 AS middle_name, + ccf.cf_1263 AS birthplace, + ccf.cf_1257 AS inn, + ccf.cf_1849 AS requisites, + ccf.cf_1580 AS code, + ccf.cf_1706 AS sms, + ccf.cf_2624 AS cf_2624 +FROM vtiger_contactdetails cd +LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid +LEFT JOIN vtiger_contactsubdetails cs ON cs.contactsubscriptionid = cd.contactid +LEFT JOIN vtiger_contactaddress ca ON ca.contactaddressid = cd.contactid +LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid +WHERE cd.contactid = $1 + AND ce.deleted = 0 +LIMIT 1; +``` + +**Параметры запроса:** +- `$1` = `{{ JSON.parse($node["CreateWebContacКлиентправ"].json.result).contact_id }}` + +--- + +### 2. Порядок нод в workflow + +1. **CreateWebContacКлиентправ** → создаёт/находит контакт +2. **PostgreSQL: Get Contact Data** → получает полные данные контакта +3. **Code in JavaScriptКлиентправ** → использует данные из обеих нод + +--- + +### 3. Что получает Code node + +После добавления ноды PostgreSQL, Code node получит доступ к: +- `$('PostgreSQL: Get Contact Data').first().json` - полные данные контакта + +**Доступные поля:** +- `contactid` - ID контакта +- `firstname`, `lastname` - ФИО +- `email`, `mobile`, `phone` - Контакты +- `birthday` - Дата рождения +- `mailingstreet`, `mailingcity`, etc. - Адрес +- `middle_name` (cf_1157) - Отчество +- `birthplace` (cf_1263) - Место рождения +- `inn` (cf_1257) - ИНН +- `requisites` (cf_1849) - Реквизиты +- `code` (cf_1580) - Код +- `sms` (cf_1706) - SMS +- `cf_2624` - Данные подтверждены + +--- + +### 4. Использование в Code node + +Код в "Code in JavaScriptКлиентправ" автоматически найдёт данные из PostgreSQL ноды и добавит их в `sessionData.contact_from_db`. + +--- + +## Альтернатива: если нет доступа к PostgreSQL + +Если нет прямого доступа к PostgreSQL, можно использовать HTTP Request к backend API: + +**Название ноды:** `HTTP Request: Get Contact Data` + +**Метод:** GET +**URL:** `{{ $env.BACKEND_URL }}/api/v1/contacts/{{ JSON.parse($node["CreateWebContacКлиентправ"].json.result).contact_id }}` + +Но лучше использовать PostgreSQL напрямую для скорости. + diff --git a/ticket_form/docs/N8N_WORKFLOW_UPDATE_CONTACT_DATA_CONFIRMED.md b/ticket_form/docs/N8N_WORKFLOW_UPDATE_CONTACT_DATA_CONFIRMED.md new file mode 100644 index 00000000..9ff65ac7 --- /dev/null +++ b/ticket_form/docs/N8N_WORKFLOW_UPDATE_CONTACT_DATA_CONFIRMED.md @@ -0,0 +1,87 @@ +# Обновление workflow 6mxRJ2LLHmQXyaDz: Подтверждение данных контакта + +## Изменения в workflow + +### 1. После ноды `CreateWebContacКлиентправ` + +**Добавить ноду:** `PostgreSQL: Auto Confirm if CRM has data` + +**SQL запрос:** +```sql +SELECT clpr_auto_confirm_if_crm_has_data($1, $2::integer); +``` + +**Параметры:** +- `$1` = `{{ $('user_get').first().json.unified_id }}` +- `$2` = `{{ JSON.parse($node['CreateWebContacКлиентправ'].json.result).contact_id }}` + +**Назначение:** Если данные уже есть в CRM (contact_id > 0), автоматически ставим флаг подтверждения. + +--- + +### 2. После ноды `Code in JavaScriptКлиентправ` + +**Добавить ноду:** `PostgreSQL: Check Contact Data Status` + +**SQL запрос:** +```sql +SELECT * FROM clpr_get_contact_data_status($1); +``` + +**Параметры:** +- `$1` = `{{ $('user_get').first().json.unified_id }}` + +**Назначение:** Проверяем, подтверждены ли данные. Результат передаём дальше. + +--- + +### 3. В ответе для фронтенда (нода `Code in JavaScript`) + +**Добавить в return:** +```javascript +const contactStatus = $('PostgreSQL: Check Contact Data Status').first().json; + +return { + // ... существующие поля ... + contact_data_confirmed: contactStatus.is_confirmed || false, + contact_data_can_edit: contactStatus.can_edit !== false, + contact_data_confirmed_at: contactStatus.confirmed_at || null +}; +``` + +--- + +### 4. После подтверждения формы (workflow для `claim_confirmed`) + +**Добавить ноду:** `PostgreSQL: Set Contact Data Confirmed` + +**SQL запрос:** +```sql +SELECT clpr_set_contact_data_confirmed($1, NOW()); +``` + +**Параметры:** +- `$1` = `{{ $json.unified_id }}` + +**Назначение:** Устанавливаем флаг подтверждения после успешного сохранения данных. + +--- + +## Порядок выполнения + +1. **Создание контакта** → `CreateWebContacКлиентправ` +2. **Автоподтверждение** → Если данные есть в CRM → `clpr_auto_confirm_if_crm_has_data` +3. **Проверка статуса** → `clpr_get_contact_data_status` → передаём фронтенду +4. **Фронтенд** → Если `contact_data_confirmed = true` → блокируем редактирование +5. **После подтверждения** → `clpr_set_contact_data_confirmed` → устанавливаем флаг + +--- + +## Проверка в n8n + +После обновления workflow проверить: +- ✅ Флаг устанавливается при наличии данных в CRM +- ✅ Флаг устанавливается после подтверждения формы +- ✅ Статус передаётся фронтенду +- ✅ Фронтенд блокирует редактирование при `contact_data_confirmed = true` + diff --git a/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx b/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx index 572f36d4..63097a2c 100644 --- a/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx +++ b/ticket_form/frontend/src/components/form/StepClaimConfirmation.tsx @@ -86,8 +86,14 @@ export default function StepClaimConfirmation({ console.log('📋 formData.propertyName:', formData.propertyName); console.log('📋 formData.propertyName?.meta:', formData.propertyName?.meta); + // ✅ Получаем флаги подтверждения данных из claimPlanData или formData + const contact_data_confirmed = + claimPlanData?.contact_data_confirmed || + claimPlanData?.propertyName?.meta?.contact_data_confirmed || + false; + // Генерируем HTML форму здесь, на нашей стороне - const html = generateConfirmationFormHTML(formData); + const html = generateConfirmationFormHTML(formData, contact_data_confirmed); setHtmlContent(html); setLoading(false); }, [claimPlanData]); diff --git a/ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts b/ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts index f6df339d..731740ab 100644 --- a/ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts +++ b/ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts @@ -1,7 +1,7 @@ // Функция генерации HTML формы подтверждения заявления // Основана на структуре из n8n Code node "Mini-app Подтверждение данных" -export function generateConfirmationFormHTML(data: any): string { +export function generateConfirmationFormHTML(data: any, contact_data_confirmed: boolean = false): string { // Извлекаем SMS данные (до нормализации, так как структура может быть разной) const smsInputData = { prefix: data.sms_meta?.prefix || data.prefix || '', @@ -290,6 +290,7 @@ export function generateConfirmationFormHTML(data: any): string { telegram_id: telegramId, token: data.token || '', sms_meta: smsMetaData, + contact_data_confirmed: contact_data_confirmed || false, // ✅ Флаг подтверждения данных контакта }); caseJson = caseJson.replace(/'; + } + return ''; } @@ -808,6 +836,9 @@ export function generateConfirmationFormHTML(data: any): string { console.log('injected.case:', injected.case); console.log('injected.propertyName:', injected.propertyName); + // ✅ Извлекаем флаг подтверждения данных из injected + var contact_data_confirmed = injected.contact_data_confirmed || false; + // Достаём объект кейса из «типичных» мест var dataCandidate = null; @@ -872,6 +903,17 @@ export function generateConfirmationFormHTML(data: any): string { var html = ''; + // ✅ Предупреждение о заблокированных данных (если данные подтверждены) + if (contact_data_confirmed) { + html += '
'; + html += '

'; + html += '⚠️ Данные подтверждены
'; + html += 'Ваши персональные данные (ФИО, ИНН, дата рождения, адрес) заблокированы для редактирования. '; + html += 'Для изменения данных обратитесь в поддержку.'; + html += '

'; + html += '
'; + } + html += '
'; html += '

В МОО «Клиентправ»

'; html += '

help@clientright.ru

'; diff --git a/ticket_form/frontend/src/pages/ClaimForm.tsx b/ticket_form/frontend/src/pages/ClaimForm.tsx index 6757a728..495011df 100644 --- a/ticket_form/frontend/src/pages/ClaimForm.tsx +++ b/ticket_form/frontend/src/pages/ClaimForm.tsx @@ -560,6 +560,19 @@ export default function ClaimForm() { const claim = data.claim; const payload = claim.payload || {}; + // ✅ Сохраняем флаги подтверждения данных контакта + const contact_data_confirmed = data.contact_data_confirmed || false; + const contact_data_can_edit = data.contact_data_can_edit !== false; // По умолчанию true + const contact_data_confirmed_at = data.contact_data_confirmed_at || null; + const contact_data_from_crm = data.contact_data_from_crm || null; + + console.log('🔒 Статус данных контакта:', { + contact_data_confirmed, + contact_data_can_edit, + contact_data_confirmed_at, + has_crm_data: !!contact_data_from_crm + }); + // ✅ Для telegram черновиков данные могут быть в payload.body const body = payload.body || {}; const isTelegramFormat = !!payload.body; @@ -806,10 +819,33 @@ export default function ClaimForm() { console.log('✅ claimPlanData для формы подтверждения:', claimPlanData); + // ✅ Если данные подтверждены и есть данные из CRM - используем их + if (contact_data_confirmed && contact_data_from_crm) { + // Обновляем applicant данные из CRM + if (claimPlanData?.propertyName?.applicant) { + claimPlanData.propertyName.applicant = { + ...claimPlanData.propertyName.applicant, + first_name: contact_data_from_crm.firstname || claimPlanData.propertyName.applicant.first_name, + last_name: contact_data_from_crm.lastname || claimPlanData.propertyName.applicant.last_name, + middle_name: contact_data_from_crm.cf_1157 || claimPlanData.propertyName.applicant.middle_name, + inn: contact_data_from_crm.cf_1257 || claimPlanData.propertyName.applicant.inn, + birth_date: contact_data_from_crm.birthday || claimPlanData.propertyName.applicant.birth_date, + birth_place: contact_data_from_crm.cf_1263 || claimPlanData.propertyName.applicant.birth_place, + address: contact_data_from_crm.mailingstreet || claimPlanData.propertyName.applicant.address, + email: contact_data_from_crm.email || claimPlanData.propertyName.applicant.email, + phone: contact_data_from_crm.mobile || claimPlanData.propertyName.applicant.phone, + }; + } + } + // Сохраняем данные заявления в formData updateFormData({ claimPlanData: claimPlanData, showClaimConfirmation: true, + // ✅ Флаги подтверждения данных + contact_data_confirmed: contact_data_confirmed, + contact_data_can_edit: contact_data_can_edit, + contact_data_confirmed_at: contact_data_confirmed_at, }); // Переход к шагу подтверждения произойдёт автоматически через useEffect