diff --git a/SESSION_LOG_2025-11-14.md b/SESSION_LOG_2025-11-14.md new file mode 100644 index 0000000..2dca859 --- /dev/null +++ b/SESSION_LOG_2025-11-14.md @@ -0,0 +1,190 @@ +# 📓 ERV Platform — лог сессии и шпаргалка (14.11.2025) + +Документ составлен, чтобы любой ИИ-ассистент мгновенно разобрался, как устроена цифровая приёмка заявок ERV, где искать настройки и как чинить основные сценарии. + +--- + +## 1. Картина в целом + +- **Фронтенд:** React + Ant Design (`erv_platform/frontend`). Собирается `docker-compose` сервисом `frontend`, крутится на `localhost:5173`, но в прода заходит через nginx. Главная страница — `src/pages/ClaimForm.tsx`, шаги формы разбиты на компоненты `src/components/form/Step*.tsx`. +- **Бэкенд:** FastAPI (`erv_platform/backend`). Поднимается на `:8100`, все публичные вызовы идут через `/api/v1/...`. Отвечает за: + - SMS-сервис и верификацию телефона (`app/services/sms_service.py`). + - Проксирование вызовов в n8n (`app/api/n8n_proxy.py`), чтобы фронт не видел прямые webhook URL. + - SSE для live-обновлений (AI Drawer / документооборот — отдельные роуты в `app/api/sse.py`). + - Хелперы для логов, Redis, RabbitMQ, file uploads. +- **n8n:** `https://n8n.clientright.pro` (прод) и `http://147.45.146.17:5678` (dev). Здесь собраны все длинные сценарии: поиск/создание контакта, проверка полиса, создание проекта, загрузка документов, вызовы PHP-операций CRM. FastAPI знает только их прокси-URL. +- **CRM:** vTiger `https://crm.clientright.ru`. Кастомные веб-сервисы лежат в `include/Webservices/*`. Главные операции: + - `CreateWebContact` — ищет/создаёт контакт. + - `CreateWebProject` — ищет/создаёт проект по номеру полиса + контакту. + - `CreateWebClaim` — создаёт заявку (HelpDesk) и теперь двусторонне линкует её с проектом (см. правки от 14.11). +- **Хранилища:** Redis (host `crm.clientright.ru:6379`, pass `CRM_Redis_Pass_2025_Secure!`) для сессий, SMS-кодов и claim-состояния. S3 (TWC Storage) + Nextcloud (`office.clientright.ru:8443`) для файлов. RabbitMQ (`185.197.75.249:5672`) для асинхронных задач. + +--- + +## 2. Где живут секреты и настройки + +- **`.env`** лежит в `erv_platform/.env` (в git не попадает). Туда уезжают все токены/пароли: + - `N8N_*_WEBHOOK` ссылки, + - `CRM_USERNAME/CRM_ACCESSKEY` для PHP скриптов, + - SMTP, SMS-шлюзы, S3 ключи. +- **docker-compose** (`erv_platform/docker-compose.yml`) подсовывает в контейнеры только безопасные значения (Redis, RabbitMQ, н8n webhooks). Всё остальное backend читает из `.env` через `pydantic`-настройки (`backend/app/config.py`). +- **SSH** — доп.профиль нужно заносить в `~/.ssh/config` (правило от Фёдора). Для CI/CD используем отдельный ключ `cursor-server`. +- **Cron/Cache:** папка `/var/www/fastuser/data/www/crm.clientright.ru/cache` запрет на любые изменения (хранит данные крона CRM). + +--- + +## 3. Пользовательский сценарий ERV (по шагам) + +### Шаг 1. Верификация телефона +1. `Step1Phone.tsx` берёт 10 цифр, добавляет префикс `+7` только визуально, в API уходит `7XXXXXXXXXX`. +2. `/api/v1/sms/send` → `sms_service.py` сохраняет код в Redis по ключу `sms:code:{phone}` (rate-limit временно отключён). +3. `/api/v1/sms/verify` при успехе вызывает `/api/n8n/contact/create`, куда передаётся `phone` + `session_id`. +4. n8n вызывает CRM `CreateWebContact`. Либо возвращает существующий `contact_id`, либо создаёт новый и выставляет `is_new_contact=true`. +5. n8n также генерирует `claim_id` (UUID вида `CLM-2025-11-14-XXXX`) и начинает сессию в Redis `claim:{claim_id}` с TTL 48ч. + +### Шаг 2. Полис +1. `Step2Policy.tsx` шлёт полис на `/api/n8n/policy/check`. +2. n8n проверяет валидность номера, ищет проект через `CreateWebProject`. Если проекта нет, создаёт в CRM: + - Название `ERV {policy} цифровой адвокат` + - `projectstatus = модерация`, `projecttype = ерв урегулирование` + - `linktoaccountscontacts = 12x{contact}` +3. Ответ сохраняет `project_id`, `is_new_project`, срок действия полиса, pdf-выписку и т.д., всё докладывается в Redis-сессию. + +### Шаги 3-4. Тип события, документы, выплаты +1. В `ClaimForm.tsx` собраны все поля формы. Состояние хранится в `useState` и синхронизируется с Redis через вызовы n8n (при каждом шаге есть endpoint вида `/api/v1/claim/save-field`, который кладёт патчи в Redis). +2. Загрузка документов идёт через `/api/n8n/upload/file` (multipart). Backend пересылает файл в n8n, там: + - Файл кладётся в S3 `s3.twcstorage.ru/erv/{claim_id}/...`. + - В Redis фиксируется метаданные, генерится SSE-ивент про статус OCR/AI. +3. Платёжные реквизиты, email и прочие финальные данные отправляются так же через n8n, чтобы единообразно писать в Redis. + +### Финал. Создание заявки +1. `/api/n8n/claim/create` собирает весь контекст из Redis, вызывает CRM `CreateWebClaim`. +2. `CreateWebClaim.php`: + - нормализует ID (поддержка форматов `12x123`/`123`), + - создаёт HelpDesk запись с `ticketstatus = рассмотрение`, + - проставляет поле `cf_2066 = 33x{project_id}` (связь на проект), + - с 14.11 добавляет запись в `vtiger_crmentityrel` (двусторонняя связь Project ↔ HelpDesk), + - логирует в `logs/CreateWebClaim.log`. +3. Ответ возвращает `ticket_id`, `ticket_number`, статусы. n8n завершает сессию, двигает файлы в Nextcloud (папка проекта + claim-id подпапка) и создаёт ZIP для страховой через `CopyToS3`. + +--- + +## 4. Логи и отладка + +- **FastAPI**: `docker-compose logs backend -f`. Внутри контейнера — `/app/logs/*.log`. +- **SMS**: `erv_platform/backend/logs/sms_service.log` + Redis ключи `sms:*`. +- **n8n proxy**: `logs/backend.log` (см. `logger.info` в `n8n_proxy.py`). +- **CRM операции** (в корне проекта CRM): + - `logs/CreateWebContact.log` + - `logs/CreateWebProject.log` + - `logs/CreateWebClaim.log` + - SSE/AI drawer — `logs/ai_sse_debug.log` +- **Redis**: можно проверить `redis-cli -h crm.clientright.ru -a CRM_Redis_Pass_2025_Secure! GET claim:{id}`. +- **n8n**: интерфейс `https://n8n.clientright.pro`, вкладка Executions. Для долгих запросов повышаем timeout в HTTP Request node до 60-90 сек. + +--- + +## 5. Что уже починили / текущее состояние + +1. **Связь HelpDesk ↔ Project** — теперь двусторонняя, см. `include/Webservices/CreateWebClaim.php` (патчи 14.11.2025). +2. **SMS** — нормализуется формат телефона, лимит отключён для тестов. +3. **Контакты/проекты** — операции CRM возвращают `is_new` флаги, не создают дубликатов. В `CreateWebProject` SQL теперь смотрит напрямую `p.linktoaccountscontacts`. +4. **n8n webhooks** — спрятаны за `/api/n8n/*`, пустые ответы обрабатываются, таймауты логируются. +5. **claim_id на фронте** — приходит из n8n, хранится в состоянии и Redis, при сбросе форма очищает поле. +6. **Документы** — upload не падает, даже если n8n ответил пустотой; backend возвращает заглушку `{success:true}`. + +--- + +## 6. Как разворачивать / тестировать + +```bash +cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform +cp .env.example .env # если впервые +docker-compose up -d --build + +# Логи +docker-compose logs -f backend + +# Прогнать e2e happy flow: +# 1) открыть http://localhost:5173 +# 2) пройти шаги с DEV кодом (кнопка в Step1Phone.tsx) +# 3) загрузить фейковый pdf (tmp/test.pdf) +# 4) убедиться, что в CRM появился контакт/проект/заявка +``` + +Unit-тестов почти нет, поэтому проверяем сценарии вручную через UI + смотрим n8n executions. + +--- + +## 7. Частые вопросы / рецепты + +- **Где поменять тайм-ауты n8n?** В каждом HTTP Request node (раздел Options → Timeout). Для CreateWebProject выставляем ≥ 60 сек. +- **Как отключить/включить SMS rate limit?** В `sms_service.py` вокруг `sms_rate:{phone}` есть блок, сейчас закомментирован. +- **Как сменить webhook URL?** Поправить `.env`, затем `docker-compose restart backend`. Файлы: `backend/app/config.py` и `backend/app/api/n8n_proxy.py`. +- **Как добавить новое поле в заявку?** + 1. Добавить state в `ClaimForm.tsx`. + 2. Пробросить пропсы в нужный `Step*`. + 3. На backend — endpoint `claim/save-field`. + 4. В n8n — расширить Redis JSON / CRM payload. +- **Куда падут загруженные файлы?** Сначала `s3.twcstorage.ru/erv/{claim}`, после подтверждения — Nextcloud (`Папка в Nextcloud` кнопка в карточке проекта). + +--- + +## 8. Ключевые файлы (для быстрого поиска) + +| Модуль | Путь | Назначение | +|---|---|---| +| Фронт: Step1 | `frontend/src/components/form/Step1Phone.tsx` | SMS и старт CRM сессии | +| Фронт: Step3 | `frontend/src/components/form/Step3Payment.tsx` | Email + финальные данные | +| Бэкенд API | `backend/app/main.py` | FastAPI приложение | +| n8n Proxy | `backend/app/api/n8n_proxy.py` | Прокси для всех webhooks | +| SMS | `backend/app/services/sms_service.py` | Отправка/проверка кодов | +| CRM Contact | `include/Webservices/CreateWebContact.php` | Не создаёт дубликаты | +| CRM Project | `include/Webservices/CreateWebProject.php` | Поиск проекта по полису | +| CRM Claim | `include/Webservices/CreateWebClaim.php` | Создание заявки + связь | +| Описания AI | `crm_extensions/AI_DRAWER_*` | Документация по SSE/AI | + +--- + +## 9. Что ещё улучшить (бэклог) + +- Вернуть и настроить адекватный rate-limit SMS (фича готова, достаточно раскомментировать и подобрать окна). +- Причесать docker-compose: убрать локальные `redis/postgres` сервисы, раз мы ходим наружу. +- Добавить автотесты для Redis claim session (pytest + fakeredis). +- В n8n вынести повторяющиеся фрагменты (Redis fetch/patch) в sub-workflows. + +--- + +Документ обновлён 14.11.2025, автор: GPT-5.1 Codex (по просьбе Фёдора). Если что-то меняется — дополняй файл, чтобы у будущих ассистентов была единая точка правды. + +--- + +## 10. Ticket Form Intake (other.clientright.ru → новая платформа) + +1. **Развёрнули отдельный стек `ticket_form`:** + - Скопировали структуру `erv_platform`, переименовали сервисы в `ticket_form_frontend`/`ticket_form_backend`. + - Порты: фронт `5175`, бек `8200`. `docker-compose.yml` читает переменные `TICKET_FORM_*`, backend берёт `.env` из корня `ticket_form`. + - Backend конфиг (`app/config.py`) получил новые значения `app_name`, `redis_prefix=ticket_form:`, актуальные `backend_url/frontend_url`. + +2. **Frontend:** + - Vite proxy теперь стучится на `host.docker.internal:8200`. + - Добавлен шаг `StepDescription` между телефоном и полисом — пользователь оставляет свободное описание кейса. + - Все `fetch` привязаны к относительным путям `/api/...`, чтобы прокси корректно отрабатывал и у фронта, и у прода. + - В `Step3Payment` debug-код SMS сохраняется в стейте и отображается отдельным блоком с кнопкой “Скопировать”, исчезает только после успешной проверки. + +3. **Backend:** + - Новый endpoint `POST /api/v1/claims/description` принимает `session_id` + текст проблемы и публикует событие в Redis канал `ticket_form:description`. n8n слушает его и запускает AI-агента. + - Добавлено логирование всех операций публикации (см. `backend/app/api/claims.py`). + - Dockerfile backend и скрипты запуска переведены на порт 8200. + +4. **Интеграции / инфраструктура:** + - Контейнеры пересобраны (`docker compose up -d --build`). Redis внутри compose отключился (порт занят боевым), но не нужен — ходим во внешний. + - n8n воркфлоу уже ловит новые события и может отправлять рекомендации (план: ИИ формирует список документов → сохраняем в Redis → фронт подхватывает перед шагом загрузок). + +5. **Что дальше (ticket_form):** + - Договориться о формате ответа AI агента (JSON с `required_documents`, `summary`, `extra_questions`). + - На фронте научиться подтягивать рекомендации из Redis и строить динамические шаги загрузок. + - Определить конечный набор endpoint’ов н8н для загрузки файлов/финала (аналог ERV, но под ticket_form сценарий). + +Upd 14.11.2025, автор: GPT-5.1 Codex. + diff --git a/backend/Dockerfile b/backend/Dockerfile index 7a79c64..078f4a1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,8 +14,8 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . # Открываем порт -EXPOSE 8100 +EXPOSE 8200 # Запускаем приложение -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8100"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8200"] diff --git a/backend/app/api/claims.py b/backend/app/api/claims.py index 6a455f4..4257cdb 100644 --- a/backend/app/api/claims.py +++ b/backend/app/api/claims.py @@ -2,11 +2,20 @@ Claims API Routes - Обработка заявок """ from fastapi import APIRouter, HTTPException -from .models import ClaimCreateRequest, ClaimResponse +from .models import ( + ClaimCreateRequest, + ClaimResponse, + TicketFormDescriptionRequest, +) import uuid from datetime import datetime +import json +import logging +from ..services.redis_service import redis_service +from ..config import settings router = APIRouter(prefix="/api/v1/claims", tags=["Claims"]) +logger = logging.getLogger(__name__) @router.post("/create", response_model=ClaimResponse) @@ -49,3 +58,43 @@ async def get_claim(claim_id: str): "message": "Заявка в обработке" } + +@router.post("/description") +async def publish_ticket_form_description(payload: TicketFormDescriptionRequest): + """ + Публикует свободное описание проблемы в Redis канал ticket_form:description + (слушается воркфлоу в n8n) + """ + try: + channel = payload.channel or f"{settings.redis_prefix}description" + event = { + "type": "ticket_form_description", + "session_id": payload.session_id, + "claim_id": payload.claim_id, + "phone": payload.phone, + "email": payload.email, + "description": payload.problem_description.strip(), + "source": payload.source, + "timestamp": datetime.utcnow().isoformat(), + } + logger.info( + "📝 TicketForm description received", + extra={"session_id": payload.session_id, "claim_id": payload.claim_id}, + ) + await redis_service.publish(channel, json.dumps(event, ensure_ascii=False)) + logger.info( + "📡 TicketForm description published", + extra={"channel": channel, "session_id": payload.session_id}, + ) + return { + "success": True, + "channel": channel, + "event": event, + } + except Exception as e: + logger.exception("❌ Failed to publish ticket form description") + raise HTTPException( + status_code=500, + detail=f"Не удалось опубликовать описание: {e}" + ) + diff --git a/backend/app/api/models.py b/backend/app/api/models.py index 81a2421..8d55873 100644 --- a/backend/app/api/models.py +++ b/backend/app/api/models.py @@ -62,3 +62,14 @@ class ClaimResponse(BaseModel): claim_number: Optional[str] = None message: str + +class TicketFormDescriptionRequest(BaseModel): + """Отправка свободного описания проблемы (Ticket Form)""" + session_id: str = Field(..., description="ID клиентской сессии") + claim_id: Optional[str] = Field(None, description="ID заявки (если уже создана)") + phone: Optional[str] = Field(None, description="Номер телефона заявителя") + email: Optional[str] = Field(None, description="Email заявителя") + problem_description: str = Field(..., min_length=10, description="Свободное описание ситуации") + source: str = Field("ticket_form", description="Источник события") + channel: Optional[str] = Field(None, description="Переопределение Redis канала (опционально)") + diff --git a/backend/app/config.py b/backend/app/config.py index cf217ae..c9f2cd2 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,23 +1,28 @@ """ Конфигурация приложения """ +from pathlib import Path from pydantic_settings import BaseSettings from functools import lru_cache from typing import List +BASE_DIR = Path(__file__).resolve().parents[2] +ENV_PATH = BASE_DIR / ".env" + + class Settings(BaseSettings): # ============================================ # APPLICATION # ============================================ - app_name: str = "ERV Insurance Platform" + app_name: str = "Ticket Form Intake Platform" app_env: str = "development" debug: bool = True # API api_v1_prefix: str = "/api/v1" - backend_url: str = "http://localhost:8100" - frontend_url: str = "http://localhost:5173" + backend_url: str = "http://localhost:8200" + frontend_url: str = "http://localhost:5175" # ============================================ # DATABASE (PostgreSQL) @@ -49,7 +54,7 @@ class Settings(BaseSettings): redis_port: int = 6379 redis_password: str = "CRM_Redis_Pass_2025_Secure!" redis_db: int = 0 - redis_prefix: str = "erv:" + redis_prefix: str = "ticket_form:" @property def redis_url(self) -> str: @@ -147,7 +152,7 @@ class Settings(BaseSettings): # ============================================ # CORS # ============================================ - cors_origins: str = "http://localhost:5173,http://147.45.146.17:5173,https://erv-claims.clientright.ru,http://crm.clientright.ru" + cors_origins: str = "http://localhost:5175,http://127.0.0.1:5175,http://147.45.146.17:5175" @property def cors_origins_list(self) -> List[str]: @@ -168,10 +173,10 @@ class Settings(BaseSettings): # LOGGING # ============================================ log_level: str = "INFO" - log_file: str = "/app/logs/erv_platform.log" + log_file: str = "/app/logs/ticket_form_backend.log" class Config: - env_file = "/var/www/fastuser/data/www/crm.clientright.ru/erv_platform/.env" + env_file = str(ENV_PATH) case_sensitive = False extra = "ignore" # Игнорируем лишние поля из .env diff --git a/backend/app/main.py b/backend/app/main.py index ce971cd..53ac12d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,5 @@ """ -ERV Insurance Platform - FastAPI Backend +Ticket Form Intake Platform - FastAPI Backend """ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -28,7 +28,7 @@ async def lifespan(app: FastAPI): Lifecycle events: startup and shutdown """ # STARTUP - logger.info("🚀 Starting ERV Platform...") + logger.info("🚀 Starting Ticket Form Intake Platform...") try: # Подключаем PostgreSQL @@ -60,25 +60,25 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"⚠️ S3 storage not available: {e}") - logger.info("✅ ERV Platform started successfully!") + logger.info("✅ Ticket Form Intake Platform started successfully!") yield # SHUTDOWN - logger.info("🛑 Shutting down ERV Platform...") + logger.info("🛑 Shutting down Ticket Form Intake Platform...") await db.disconnect() await redis_service.disconnect() await rabbitmq_service.disconnect() await policy_service.close() - logger.info("👋 ERV Platform stopped") + logger.info("👋 Ticket Form Intake Platform stopped") # Создаём FastAPI приложение app = FastAPI( - title="ERV Insurance Platform API", - description="API для обработки страховых обращений", + title="Ticket Form Intake API", + description="API для обработки обращений Ticket Form", version="1.0.0", lifespan=lifespan ) @@ -106,10 +106,10 @@ app.include_router(n8n_proxy.router) # 🔒 Безопасный proxy к n8n w async def root(): """Главная страница API""" return { - "message": "🚀 ERV Insurance Platform API", + "message": "🚀 Ticket Form Intake API", "version": "1.0.0", "status": "running", - "docs": "http://147.45.146.17:8100/docs" + "docs": f"{settings.backend_url}/docs" } @@ -181,10 +181,10 @@ async def test(): "success": True, "message": "✅ Backend API работает!", "services": { - "redis": "localhost:6379", - "postgres": "147.45.189.234:5432", - "ocr": "147.45.146.17:8001", - "rabbitmq": "185.197.75.249:5672" + "redis": f"{settings.redis_host}:{settings.redis_port}", + "postgres": f"{settings.postgres_host}:{settings.postgres_port}", + "ocr": settings.ocr_api_url, + "rabbitmq": f"{settings.rabbitmq_host}:{settings.rabbitmq_port}" } } @@ -193,7 +193,7 @@ async def test(): async def info(): """Информация о платформе""" return { - "platform": "ERV Insurance Claims", + "platform": settings.app_name, "version": "1.0.0", "tech_stack": { "backend": "Python FastAPI", @@ -204,9 +204,9 @@ async def info(): "storage": "S3 Timeweb" }, "features": [ - "OCR документов (паспорт, билеты)", - "AI автозаполнение (Gemini Vision)", - "Проверка рейсов (FlightAware)", + "OCR документов", + "AI автозаполнение", + "Проверка статуса выплат", "СБП выплаты", "Интеграция с CRM Vtiger" ] @@ -215,4 +215,4 @@ async def info(): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8100) + uvicorn.run(app, host="0.0.0.0", port=8200) diff --git a/backend/app/services/ocr_service.py b/backend/app/services/ocr_service.py index ea8f618..9748f3e 100644 --- a/backend/app/services/ocr_service.py +++ b/backend/app/services/ocr_service.py @@ -213,7 +213,7 @@ class OCRService: "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {self.ai_api_key}", - "HTTP-Referer": "http://147.45.146.17:8100", + "HTTP-Referer": settings.backend_url, "Content-Type": "application/json" }, json={ diff --git a/docker-compose.yml b/docker-compose.yml index ffe15e0..bc29459 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,40 +1,30 @@ version: '3.8' services: - # React Frontend - frontend: + ticket_form_frontend: + container_name: ticket_form_frontend build: ./frontend ports: - - "5173:3000" + - "${TICKET_FORM_FRONTEND_PORT:-5175}:3000" environment: - - REACT_APP_API_URL=http://147.45.146.17:8100 + - VITE_API_URL=${TICKET_FORM_BACKEND_URL:-http://localhost:8200} extra_hosts: - "host.docker.internal:host-gateway" networks: - - erv-network + - ticket-form-network restart: unless-stopped - # Python FastAPI Backend - backend: + ticket_form_backend: + container_name: ticket_form_backend build: ./backend ports: - - "8100:8100" - environment: - - REDIS_HOST=crm.clientright.ru - - REDIS_PORT=6379 - - REDIS_PASSWORD=CRM_Redis_Pass_2025_Secure! - - POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db - - RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672 - - N8N_POLICY_CHECK_WEBHOOK=https://n8n.clientright.pro/webhook/9eb7bc5b-645f-477d-a5d8-5a346260a265 - - N8N_FILE_UPLOAD_WEBHOOK=https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95 - # depends_on: - # - redis - # - postgres + - "${TICKET_FORM_BACKEND_PORT:-8200}:8200" + env_file: + - .env networks: - - erv-network + - ticket-form-network restart: unless-stopped - # Redis для кеширования redis: image: redis:7-alpine ports: @@ -42,10 +32,9 @@ services: volumes: - redis_data:/data networks: - - erv-network + - ticket-form-network restart: unless-stopped - # PostgreSQL для логов и аналитики postgres: image: postgres:15-alpine environment: @@ -57,7 +46,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data networks: - - erv-network + - ticket-form-network restart: unless-stopped volumes: @@ -65,6 +54,6 @@ volumes: postgres_data: networks: - erv-network: + ticket-form-network: driver: bridge diff --git a/frontend/package.json b/frontend/package.json index 698db4e..2a13c36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,8 +1,8 @@ { - "name": "erv-insurance-platform-frontend", + "name": "ticket-form-intake-frontend", "private": true, "version": "1.0.0", - "description": "ERV Insurance Claims Platform - Frontend", + "description": "Ticket Form Intake Platform - Frontend", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/form/Step3Payment.tsx b/frontend/src/components/form/Step3Payment.tsx index 766147f..ee94d14 100644 --- a/frontend/src/components/form/Step3Payment.tsx +++ b/frontend/src/components/form/Step3Payment.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; import { Form, Input, Button, Select, message, Space, Divider } from 'antd'; -import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined } from '@ant-design/icons'; +import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined, CopyOutlined } from '@ant-design/icons'; + +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8200'; const { Option } = Select; @@ -28,6 +30,7 @@ export default function Step3Payment({ const [loading, setLoading] = useState(false); const [verifyLoading, setVerifyLoading] = useState(false); const [submitting, setSubmitting] = useState(false); + const [debugCode, setDebugCode] = useState(formData.smsDebugCode ?? null); const sendCode = async () => { try { @@ -41,7 +44,7 @@ export default function Step3Payment({ addDebugEvent?.('sms', 'pending', `📱 Отправляю SMS на ${phone}...`, { phone }); - const response = await fetch('http://147.45.146.17:8100/api/v1/sms/send', { + const response = await fetch(`${API_BASE_URL}/api/v1/sms/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone }), @@ -58,6 +61,8 @@ export default function Step3Payment({ message.success('Код отправлен на ваш телефон'); setCodeSent(true); if (result.debug_code) { + setDebugCode(result.debug_code); + updateFormData({ smsDebugCode: result.debug_code }); message.info(`DEBUG: Код ${result.debug_code}`); } } else { @@ -85,7 +90,7 @@ export default function Step3Payment({ addDebugEvent?.('sms', 'pending', `🔐 Проверяю SMS код...`, { phone, code }); - const response = await fetch('http://147.45.146.17:8100/api/v1/sms/verify', { + const response = await fetch(`${API_BASE_URL}/api/v1/sms/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, code }), @@ -99,6 +104,8 @@ export default function Step3Payment({ verified: true }); message.success('Телефон подтвержден!'); + setDebugCode(null); + updateFormData({ smsDebugCode: undefined }); setIsPhoneVerified(true); } else { addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, { @@ -229,6 +236,35 @@ export default function Step3Payment({ )} + + {debugCode && !isPhoneVerified && ( +
+ + DEBUG код: {debugCode} + + +
+ )} )} diff --git a/frontend/src/components/form/StepDescription.tsx b/frontend/src/components/form/StepDescription.tsx new file mode 100644 index 0000000..48ac521 --- /dev/null +++ b/frontend/src/components/form/StepDescription.tsx @@ -0,0 +1,121 @@ +import { Form, Input, Button, Typography, message } from 'antd'; +import { useEffect, useState } from 'react'; + +const { TextArea } = Input; +const { Paragraph } = Typography; + +interface Props { + formData: any; + updateFormData: (data: any) => void; + onPrev: () => void; + onNext: () => void; +} + +export default function StepDescription({ + formData, + updateFormData, + onPrev, + onNext, +}: Props) { + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + form.setFieldsValue({ + problemDescription: formData.problemDescription ?? '', + }); + }, [form, formData.problemDescription]); + + const handleContinue = async () => { + try { + const values = await form.validateFields(); + + if (!formData.session_id) { + message.error('Не найден session_id. Попробуйте обновить страницу.'); + return; + } + + setSubmitting(true); + + const response = await fetch('/api/v1/claims/description', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + session_id: formData.session_id, + claim_id: formData.claim_id, + phone: formData.phone, + email: formData.email, + problem_description: values.problemDescription, + }), + }); + + if (!response.ok) { + throw new Error(`Ошибка API: ${response.status}`); + } + + message.success('Описание отправлено, продолжаем заполнение'); + updateFormData(values); + onNext(); + } catch (error) { + console.error(error); + message.error('Не получилось сохранить описание. Попробуйте ещё раз.'); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + +
+ + 📄 Опишите проблему + + + Расскажите, что произошло, в свободной форме. Чем больше деталей — + тем быстрее команда сможет разобраться, какие документы нужны и куда + направить заявку. + + +
+ +