diff --git a/SESSION_LOG_2025-11-14.md b/SESSION_LOG_2025-11-14.md index 2dca859..ece1de2 100644 --- a/SESSION_LOG_2025-11-14.md +++ b/SESSION_LOG_2025-11-14.md @@ -188,3 +188,38 @@ Unit-тестов почти нет, поэтому проверяем сцен Upd 14.11.2025, автор: GPT-5.1 Codex. +--- + +## 11. Ticket Form — доработки 15.11.2025 + +### 11.1. SSE + Wizard Plan +- Новая стадия формы `StepWizardPlan` между описанием и выбором услуги: + - подключается к `/events/{claim_id}`, выбирает payload даже если `wizard_plan` лежит в `data`, `redis_value` или `event`. + - отображает иллюстрацию/спиннер, пишет события в DebugPanel. + - при Success сохраняет `wizardPlan`, `answers_prefill`, `coverage_report`, `wizardPrefillMap` в состоянии. +- На случай отладки добавлен чекбокс в `StepDescription`: «Использовать сохранённые рекомендации (DEV)». + - По умолчанию включен; берёт мок `wizardPlanSample` (лежит в `frontend/src/mocks`), пропускает вызов AI и блокирует textarea. + - При снятом чекбоксе описание снова обязательное и реально отправляется на `/api/v1/claims/description`. + +### 11.2. Динамическая анкета +- `StepWizardPlan` строит форму исключительно из `wizard_plan.questions`: текст, textarea, радио. +- Въелся прогресс-бар с подсчётом обязательных полей (done / total). +- `wizardPlanStatus` принимает значения `pending | ready | answered`, чтобы следующие шаги понимали, прошёл ли пользователь анкету. + +### 11.3. Документы прямо в анкете +- Под вопросами «Есть ли документы?» и «Есть ли переписка?» появляются мультилоадеры: + - группы файлов с описанием, категорией (select), списком допустимых форматов, лимитом 20 МБ. + - для каждого документа из `plan.documents` можно создать несколько блоков; храним их в `wizardUploads.documents`. + - кастомная секция «Дополнительные документы» позволяет добавить произвольные блоки (категория + описание + файлы), лежат в `wizardUploads.custom`. +- Валидация: если ответ «Да», но файлы не добавлены или нет описаний — показываем ошибку, не пускаем дальше. +- До отправки (переход на следующий шаг) сохраняем `wizardUploads` для дальнейшего api/n8n. + +### 11.4. Прочее +- `ClaimForm` логи перенесены в `useEffect`, чтобы StrictMode не писал дубль. +- Кнопка «Обновить рекомендации» сбрасывает `wizardPlan` и пересоздаёт SSE. +- Docker: каждый раз после правок фронт пересобирали `docker compose build ticket_form_frontend && docker compose up -d ticket_form_frontend`. + +### TODO (перенесено в бэклог) +- На backend обезопасить хранение `wizard_plan` в Redis (по ключу `wizard_plan:{claim_id}`) и отдавать кеш при DEV-галке. +- Передать `wizardUploads` в следующий шаг & далее в n8n, чтобы фактически загрузить файлы/метаданные. + diff --git a/frontend/src/assets/ai-working.svg b/frontend/src/assets/ai-working.svg new file mode 100644 index 0000000..5bb92c0 --- /dev/null +++ b/frontend/src/assets/ai-working.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/form/StepDescription.tsx b/frontend/src/components/form/StepDescription.tsx index 48ac521..322e76d 100644 --- a/frontend/src/components/form/StepDescription.tsx +++ b/frontend/src/components/form/StepDescription.tsx @@ -1,5 +1,6 @@ -import { Form, Input, Button, Typography, message } from 'antd'; +import { Form, Input, Button, Typography, message, Checkbox } from 'antd'; import { useEffect, useState } from 'react'; +import wizardPlanSample from '../../mocks/wizardPlanSample'; const { TextArea } = Input; const { Paragraph } = Typography; @@ -19,6 +20,19 @@ export default function StepDescription({ }: Props) { const [form] = Form.useForm(); const [submitting, setSubmitting] = useState(false); + const [useMockWizard, setUseMockWizard] = useState(true); + + const buildPrefillMap = (prefill?: Array<{ name: string; value: any }>) => { + if (!prefill) { + return {}; + } + return prefill.reduce>((acc, item) => { + if (item?.name) { + acc[item.name] = item.value; + } + return acc; + }, {}); + }; useEffect(() => { form.setFieldsValue({ @@ -28,15 +42,44 @@ export default function StepDescription({ const handleContinue = async () => { try { - const values = await form.validateFields(); + let problemDescription = form.getFieldValue('problemDescription'); + if (!useMockWizard) { + const values = await form.validateFields(); + problemDescription = values.problemDescription; + } + const safeDescription = problemDescription || ''; if (!formData.session_id) { message.error('Не найден session_id. Попробуйте обновить страницу.'); return; } + if (!formData.claim_id) { + message.error('Не удалось определить номер обращения. Вернитесь на шаг с телефоном.'); + return; + } setSubmitting(true); + if (useMockWizard && wizardPlanSample?.wizard_plan) { + const mockPrefill = buildPrefillMap(wizardPlanSample.answers_prefill); + const mockClaimId = wizardPlanSample.claim_id || formData.claim_id; + + updateFormData({ + problemDescription: safeDescription, + claim_id: mockClaimId, + wizardPlan: wizardPlanSample.wizard_plan, + wizardPlanStatus: 'ready', + wizardPrefill: mockPrefill, + wizardPrefillArray: wizardPlanSample.answers_prefill, + wizardCoverageReport: wizardPlanSample.coverage_report, + wizardAnswers: undefined, + }); + + message.success('Загружены сохранённые рекомендации (DEV).'); + onNext(); + return; + } + const response = await fetch('/api/v1/claims/description', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -45,7 +88,7 @@ export default function StepDescription({ claim_id: formData.claim_id, phone: formData.phone, email: formData.email, - problem_description: values.problemDescription, + problem_description: safeDescription, }), }); @@ -53,8 +96,15 @@ export default function StepDescription({ throw new Error(`Ошибка API: ${response.status}`); } - message.success('Описание отправлено, продолжаем заполнение'); - updateFormData(values); + message.success('Описание отправлено, подбираем рекомендации...'); + updateFormData({ + problemDescription: safeDescription, + wizardPlan: undefined, + wizardPlanStatus: 'pending', + wizardAnswers: undefined, + wizardPrefill: undefined, + wizardPrefillArray: undefined, + }); onNext(); } catch (error) { console.error(error); @@ -93,22 +143,54 @@ export default function StepDescription({ label="Описание ситуации" name="problemDescription" rules={[ - { required: true, message: 'Поле обязательно' }, { - min: 20, - message: 'Опишите, пожалуйста, минимум в пару предложений', + validator: (_, value) => { + if (useMockWizard) { + return Promise.resolve(); + } + if (!value) { + return Promise.reject(new Error('Поле обязательно')); + } + if (value.length < 20) { + return Promise.reject( + new Error('Опишите, пожалуйста, минимум в пару предложений') + ); + } + return Promise.resolve(); + }, }, ]} >