fix: Исправление загрузки документов и SQL запросов

- Исправлена потеря документов при обновлении черновика (SQL объединяет вместо перезаписи)
- Исправлено определение типа документа (приоритет field_label над field_name)
- Исправлены дубликаты в documents_meta и documents_uploaded
- Добавлена передача group_index с фронтенда для правильного field_name
- Исправлены все документы в таблице clpr_claim_documents с правильными field_name
- Обновлены SQL запросы: claimsave и claimsave_final для нового флоу
- Добавлена поддержка multi-file upload для одного документа
- Исправлены дубликаты в списке загруженных документов на фронтенде

Файлы:
- SQL: SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql, SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql
- n8n: N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js (поддержка group_index)
- Backend: documents.py (передача group_index в n8n)
- Frontend: StepWizardPlan.tsx (передача group_index, исправление дубликатов)
- Скрипты: fix_claim_documents_field_names.py, fix_documents_meta_duplicates.py

Результат: документы больше не теряются, имеют правильные типы и field_name
This commit is contained in:
AI Assistant
2025-11-26 19:54:51 +03:00
parent 1d6c9d1f52
commit 02689e65db
42 changed files with 8314 additions and 232 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Card, Checkbox, Form, Input, Radio, Result, Select, Skeleton, Space, Tag, Typography, Upload, message, Progress } from 'antd';
import { LoadingOutlined, PlusOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { LoadingOutlined, PlusOutlined, ThunderboltOutlined, InboxOutlined } from '@ant-design/icons';
import AiWorkingIllustration from '../../assets/ai-working.svg';
import type { UploadFile } from 'antd/es/upload/interface';
@@ -239,17 +239,27 @@ export default function StepWizardPlan({
? docList[0].id
: docId;
handleDocumentBlocksChange(docId, (blocks) => [
...blocks,
{
id: generateBlockId(docId),
fieldName: docId,
description: '',
category: category,
docLabel: docLabel,
files: [],
},
]);
handleDocumentBlocksChange(docId, (blocks) => {
// ✅ Автогенерация уникального описания:
// - Первый блок: пустое (будет использоваться docLabel)
// - Второй и далее: "docLabel #N"
const blockNumber = blocks.length + 1;
const autoDescription = blockNumber > 1
? `${docLabel || docId} #${blockNumber}`
: '';
return [
...blocks,
{
id: generateBlockId(docId),
fieldName: docId,
description: autoDescription,
category: category,
docLabel: docLabel,
files: [],
},
];
});
};
const updateDocumentBlock = (
@@ -328,53 +338,61 @@ export default function StepWizardPlan({
setProgressState({ done, total });
}, [formValues, questions]);
// Автоматически создаём блоки для обязательных документов при ответе "Да"
// Автоматически создаём блоки для ВСЕХ документов из плана при загрузке
// Используем ref чтобы отслеживать какие блоки уже созданы
const createdDocBlocksRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!plan || !formValues) return;
if (!plan || !documents || documents.length === 0) return;
questions.forEach((question) => {
const visible = evaluateCondition(question.ask_if, formValues);
if (!visible) return;
documents.forEach((doc) => {
const docKey = doc.id || doc.name || `doc_unknown`;
const questionValue = formValues?.[question.name];
if (!isAffirmative(questionValue)) return;
// Не создаём блок, если уже создавали
if (createdDocBlocksRef.current.has(docKey)) return;
const questionDocs = documentGroups[question.name] || [];
questionDocs.forEach((doc) => {
if (!doc.required) return;
const docKey = doc.id || doc.name || `doc_${question.name}`;
// Не создаём блок, если документ пропущен
if (skippedDocuments.has(docKey)) return;
const existingBlocks = questionFileBlocks[docKey] || [];
// Если блока ещё нет, создаём его автоматически
if (existingBlocks.length === 0) {
const category = doc.id && !doc.id.includes('_exist') ? doc.id : docKey;
handleDocumentBlocksChange(docKey, (blocks) => [
...blocks,
{
id: generateBlockId(docKey),
fieldName: docKey,
description: '',
category: category,
docLabel: doc.name,
files: [],
},
]);
}
// Не создаём блок, если документ пропущен
if (skippedDocuments.has(docKey)) return;
// Помечаем как созданный
createdDocBlocksRef.current.add(docKey);
const category = doc.id && !doc.id.includes('_exist') ? doc.id : docKey;
handleDocumentBlocksChange(docKey, (blocks) => {
// Проверяем ещё раз внутри callback
if (blocks.length > 0) return blocks;
return [
...blocks,
{
id: generateBlockId(docKey),
fieldName: docKey,
description: '',
category: category,
docLabel: doc.name,
files: [],
},
];
});
});
}, [formValues, plan, questions, documentGroups, questionFileBlocks, handleDocumentBlocksChange, skippedDocuments]);
}, [plan, documents, handleDocumentBlocksChange, skippedDocuments]);
useEffect(() => {
if (!isWaiting || !formData.session_id || plan) {
console.log('⏭️ StepWizardPlan: пропускаем подписку SSE', {
isWaiting,
hasSessionId: !!formData.session_id,
hasPlan: !!plan,
});
return;
}
const sessionId = formData.session_id;
console.log('🔌 StepWizardPlan: подписываемся на SSE канал для получения wizard_plan', {
session_id: sessionId,
sse_url: `/events/${sessionId}`,
redis_channel: `ocr_events:${sessionId}`,
});
const source = new EventSource(`/events/${sessionId}`);
eventSourceRef.current = source;
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { session_id: sessionId });
@@ -441,6 +459,43 @@ export default function StepWizardPlan({
payload_preview: JSON.stringify(payload).substring(0, 200),
});
// ✅ НОВЫЙ ФЛОУ: Обработка списка документов
if (eventType === 'documents_list_ready') {
const documentsRequired = payload.documents_required || [];
debugLoggerRef.current?.('wizard', 'success', '📋 Получен список документов!', {
session_id: sessionId,
documents_count: documentsRequired.length,
documents: documentsRequired.map((d: any) => d.name),
});
console.log('📋 documents_list_ready:', {
claim_id: payload.claim_id,
documents_required: documentsRequired,
});
// Сохраняем в formData для нового флоу
updateFormData({
documents_required: documentsRequired,
claim_id: payload.claim_id,
wizardPlanStatus: 'documents_ready', // Новый статус
});
setIsWaiting(false);
setConnectionError(null);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
// Пока показываем alert для теста, потом переход к StepDocumentsNew
message.success(`Получен список документов: ${documentsRequired.length} шт.`);
// TODO: onNext() для перехода к StepDocumentsNew
return;
}
const wizardPayload = extractWizardPayload(payload);
const hasWizardPlan = Boolean(wizardPayload);
@@ -695,6 +750,17 @@ export default function StepWizardPlan({
return `upload_${group.index}`;
};
// ✅ Подсчитываем дубликаты labels для автоматической нумерации
const labelCounts: Record<string, number> = {};
const labelIndexes: Record<string, number> = {};
// Первый проход - считаем сколько раз встречается каждый label
groups.forEach((group) => {
const block = group.block;
const baseLabel = (block.description?.trim()) || block.docLabel || block.fieldName || guessFieldName(group);
labelCounts[baseLabel] = (labelCounts[baseLabel] || 0) + 1;
});
groups.forEach((group) => {
const i = group.index;
const block = group.block;
@@ -713,10 +779,29 @@ export default function StepWizardPlan({
);
// ✅ Добавляем реальное название поля (label) для использования в n8n
// Приоритет: description (если заполнено) > docLabel > fieldLabel
const baseLabel = (block.description?.trim()) || block.docLabel || fieldLabel;
// ✅ Автоматическая нумерация для дубликатов
let finalFieldLabel = baseLabel;
if (labelCounts[baseLabel] > 1) {
labelIndexes[baseLabel] = (labelIndexes[baseLabel] || 0) + 1;
finalFieldLabel = `${baseLabel} #${labelIndexes[baseLabel]}`;
}
formPayload.append(
`uploads_field_labels[${i}]`,
block.docLabel || block.description || fieldLabel
finalFieldLabel
);
// 🔍 Логируем отправляемые метаданные документов
console.log(`📁 Группа ${i}:`, {
field_name: fieldLabel,
field_label: finalFieldLabel,
description: block.description,
docLabel: block.docLabel,
filesCount: block.files.length,
});
// Файлы: uploads[i][j]
block.files.forEach((file, j) => {
@@ -919,23 +1004,19 @@ export default function StepWizardPlan({
const accept = docList.flatMap((doc) => doc.accept || []);
const uniqueAccept = Array.from(new Set(accept.length ? accept : ['pdf', 'jpg', 'png']));
// Если документ предопределён (конкретный тип, не общий), не показываем лишние поля
// Предопределённые документы: contract, payment, payment_confirmation и их вариации
// Документ предопределён если у него есть id и он НЕ общий (не содержит _exist)
// Для предустановленных документов НЕ показываем поле описания и кнопку "Удалить"
const doc = docList[0];
const isPredefinedDoc = docList.length === 1 && doc && doc.id &&
!doc.id.includes('_exist') &&
(doc.id === 'contract' || doc.id === 'payment' || doc.id === 'payment_confirmation' ||
doc.id.includes('contract') || doc.id.includes('payment') || doc.id.includes('receipt') ||
doc.id.includes('cheque') || doc.id.includes('чек'));
const singleDocName = isPredefinedDoc ? doc.name : null;
const isPredefinedDoc = docList.length === 1 && doc && doc.id && !doc.id.includes('_exist');
const singleDocName = doc?.name || docLabel;
const isRequired = docList.some(doc => doc.required);
const isSkipped = skippedDocuments.has(docId);
return (
<Space direction="vertical" style={{ width: '100%' }}>
{/* Чекбокс "Пропустить" для обязательных документов */}
{isRequired && (
<div style={{ marginBottom: 8, padding: 8, background: '#f8f9fa', borderRadius: 8 }}>
{/* Если документ пропущен - показываем только сообщение */}
{isSkipped && (
<div style={{ padding: 12, background: '#fff7e6', borderRadius: 8, border: '1px solid #ffd591' }}>
<Checkbox
checked={isSkipped}
onChange={(e) => {
@@ -949,7 +1030,7 @@ export default function StepWizardPlan({
updateFormData({ wizardSkippedDocuments: Array.from(newSkipped) });
}}
>
У меня нет этого документа
<Text type="warning">У меня нет документа: {docLabel}</Text>
</Checkbox>
</div>
)}
@@ -965,7 +1046,9 @@ export default function StepWizardPlan({
}}
title={singleDocName || `${docLabel} — группа #${idx + 1}`}
extra={
currentBlocks.length > 1 && (
// Кнопка "Удалить" только если это дополнительный блок (idx > 0)
// Первый блок предустановленного документа удалять нельзя
(currentBlocks.length > 1 && idx > 0) && (
<Button
type="link"
danger
@@ -978,11 +1061,11 @@ export default function StepWizardPlan({
}
>
<Space direction="vertical" style={{ width: '100%' }}>
{/* Поле описания только для необязательных/кастомных документов */}
{/* Для обязательных документов (contract, payment) описание не требуется */}
{!isPredefinedDoc && !isRequired && (
{/* Поле описания показываем только для дополнительных блоков (idx > 0)
или для общих документов (docs_exist) */}
{(idx > 0 || !isPredefinedDoc) && (
<Input
placeholder="Описание документов (например: договор от 12.05, платёжка №123)"
placeholder="Уточните тип документа (например: Претензия от 12.05)"
value={block.description}
onChange={(e) =>
updateDocumentBlock(docId, block.id, { description: e.target.value })
@@ -1023,6 +1106,24 @@ export default function StepWizardPlan({
Допустимые форматы: {uniqueAccept.join(', ')}. До 5 файлов, максимум 20 МБ каждый.
</p>
</Dragger>
{/* Чекбокс "Нет документа" под загрузкой - только для обязательных и только в первом блоке */}
{isRequired && idx === 0 && block.files.length === 0 && (
<Checkbox
checked={false}
onChange={(e) => {
if (e.target.checked) {
const newSkipped = new Set(skippedDocuments);
newSkipped.add(docId);
setSkippedDocuments(newSkipped);
updateFormData({ wizardSkippedDocuments: Array.from(newSkipped) });
}
}}
style={{ marginTop: 8 }}
>
<Text type="secondary">У меня нет этого документа</Text>
</Checkbox>
)}
</Space>
</Card>
))}
@@ -1170,6 +1271,17 @@ export default function StepWizardPlan({
// Если в плане визарда есть документы, не показываем поля про загрузку (text/textarea/file)
const questionLabelLower = (question.label || '').toLowerCase();
const questionNameLower = (question.name || '').toLowerCase();
// Скрываем вопрос docs_exist (чекбоксы "какие документы есть") если есть документы
// Загрузка документов реализована через отдельные блоки под информационной карточкой
const isDocsExistQuestion = questionNameLower === 'docs_exist' ||
questionNameLower === 'correspondence_exist' ||
questionNameLower.includes('docs_exist');
if (isDocsExistQuestion && documents.length > 0) {
console.log(`🚫 Question ${question.name} hidden: docs_exist with documents`);
return null;
}
const isDocumentUploadQuestion =
(question.input_type === 'text' ||
question.input_type === 'textarea' ||
@@ -1256,11 +1368,164 @@ export default function StepWizardPlan({
);
}
// ✅ НОВЫЙ ФЛОУ: Если есть documents_required, показываем загрузку документов
const documentsRequired = formData.documents_required || [];
const hasNewFlowDocs = documentsRequired.length > 0;
// 🔍 ОТЛАДКА: Логируем состояние для диагностики
console.log('🔍 StepWizardPlan - определение флоу:', {
documentsRequiredCount: documentsRequired.length,
documentsRequired: documentsRequired,
hasNewFlowDocs,
hasPlan: !!plan,
isWaiting,
formDataKeys: Object.keys(formData),
});
// Состояние для поэкранной загрузки документов (новый флоу)
const [currentDocIndex, setCurrentDocIndex] = useState(formData.current_doc_index || 0);
// Убираем дубликаты при инициализации
const initialUploadedDocs = formData.documents_uploaded?.map((d: any) => d.type || d.id) || [];
const [uploadedDocs, setUploadedDocs] = useState<string[]>(Array.from(new Set(initialUploadedDocs)));
const [skippedDocs, setSkippedDocs] = useState<string[]>(formData.documents_skipped || []);
const [docChoice, setDocChoice] = useState<'upload' | 'none'>('upload'); // Выбор: загрузить или нет документа (по умолчанию - загрузить)
const [currentUploadedFiles, setCurrentUploadedFiles] = useState<any[]>([]); // Массив загруженных файлов
// Текущий документ для загрузки
const currentDoc = documentsRequired[currentDocIndex];
const isLastDoc = currentDocIndex >= documentsRequired.length - 1;
const allDocsProcessed = currentDocIndex >= documentsRequired.length;
// Обработчик выбора файлов (НЕ отправляем сразу, только сохраняем)
const handleFilesChange = (fileList: any[]) => {
console.log('📁 handleFilesChange:', fileList.length, 'файлов', fileList.map(f => f.name));
setCurrentUploadedFiles(fileList);
if (fileList.length > 0) {
setDocChoice('upload');
}
};
// Обработчик "Продолжить" — отправляем файл или пропускаем
const handleDocContinue = async () => {
if (!currentDoc) return;
// Если выбрано "Нет документа" — пропускаем
if (docChoice === 'none') {
if (currentDoc.required) {
message.warning(`⚠️ Документ "${currentDoc.name}" важен для рассмотрения заявки. Постарайтесь найти его позже.`);
}
const newSkipped = [...skippedDocs, currentDoc.id];
setSkippedDocs(newSkipped);
updateFormData({
documents_skipped: newSkipped,
current_doc_index: currentDocIndex + 1,
});
// Переход к следующему (сброс состояния в useEffect)
setCurrentDocIndex(prev => prev + 1);
return;
}
// Если выбрано "Загрузить" — отправляем все файлы ОДНИМ запросом
if (docChoice === 'upload' && currentUploadedFiles.length > 0) {
try {
setSubmitting(true);
console.log('📤 Загружаем все файлы одним запросом:', {
totalFiles: currentUploadedFiles.length,
files: currentUploadedFiles.map(f => ({ name: f.name, uid: f.uid, size: f.size }))
});
const formDataToSend = new FormData();
formDataToSend.append('claim_id', formData.claim_id || '');
formDataToSend.append('session_id', formData.session_id || '');
formDataToSend.append('unified_id', formData.unified_id || '');
formDataToSend.append('contact_id', formData.contact_id || '');
formDataToSend.append('phone', formData.phone || '');
formDataToSend.append('document_type', currentDoc.id);
formDataToSend.append('document_name', currentDoc.name || currentDoc.id);
formDataToSend.append('document_description', currentDoc.hints || '');
formDataToSend.append('group_index', String(currentDocIndex)); // ✅ Передаём индекс документа для правильного field_name
// Добавляем все файлы в один запрос
currentUploadedFiles.forEach((file) => {
formDataToSend.append('files', file.originFileObj, file.name);
});
const response = await fetch('/api/v1/documents/upload-multiple', {
method: 'POST',
body: formDataToSend,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Ошибка загрузки файлов');
}
const result = await response.json();
console.log('✅ Все файлы загружены:', result);
// Обновляем состояние
const uploadedDocsData = [...(formData.documents_uploaded || [])];
// Добавляем информацию о каждом загруженном файле
result.file_ids.forEach((fileId: string, i: number) => {
uploadedDocsData.push({
type: currentDoc.id,
file_id: fileId,
filename: currentUploadedFiles[i]?.name || `file_${i}`,
ocr_status: 'processing',
});
});
message.success(`${currentDoc.name}: загружено ${result.files_count} файл(ов)!`);
// Убираем дубликаты при добавлении
const newUploaded = uploadedDocs.includes(currentDoc.id)
? uploadedDocs
: [...uploadedDocs, currentDoc.id];
setUploadedDocs(newUploaded);
updateFormData({
documents_uploaded: uploadedDocsData,
current_doc_index: currentDocIndex + 1,
});
// Переход к следующему (сброс состояния в useEffect)
setCurrentDocIndex(prev => prev + 1);
} catch (error: any) {
message.error(`Ошибка загрузки: ${error.message}`);
console.error('Upload error:', error);
} finally {
setSubmitting(false);
}
}
};
// Можно ли нажать "Продолжить"
const canContinue = docChoice === 'none' || (docChoice === 'upload' && currentUploadedFiles.length > 0);
// Сброс состояния при переходе к следующему документу
useEffect(() => {
setDocChoice('upload');
setCurrentUploadedFiles([]);
}, [currentDocIndex]);
// Все документы загружены — переход к ожиданию заявления
const handleAllDocsComplete = () => {
message.loading('Формируем заявление...', 0);
// TODO: Переход к StepWaitingClaim или показ loader
onNext();
};
return (
<div style={{ marginTop: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Button onClick={onPrev}> Назад</Button>
{plan && (
{plan && !hasNewFlowDocs && (
<Button type="link" onClick={handleRefreshPlan}>
Обновить рекомендации
</Button>
@@ -1274,7 +1539,143 @@ export default function StepWizardPlan({
background: '#fafafa',
}}
>
{isWaiting && (
{/* ✅ НОВЫЙ ФЛОУ: Поэкранная загрузка документов */}
{hasNewFlowDocs && !allDocsProcessed && currentDoc && (
<div style={{ padding: '24px 0' }}>
{/* Прогресс */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Text type="secondary">Документ {currentDocIndex + 1} из {documentsRequired.length}</Text>
<Text type="secondary">{Math.round((currentDocIndex / documentsRequired.length) * 100)}% завершено</Text>
</div>
<Progress
percent={Math.round((currentDocIndex / documentsRequired.length) * 100)}
showInfo={false}
strokeColor="#595959"
/>
</div>
{/* Заголовок документа */}
<Title level={4} style={{ marginBottom: 8 }}>
📄 {currentDoc.name}
{currentDoc.required && <Tag color="volcano" style={{ marginLeft: 8 }}>Важный</Tag>}
</Title>
{currentDoc.hints && (
<Paragraph type="secondary" style={{ marginBottom: 16 }}>
{currentDoc.hints}
</Paragraph>
)}
{/* Радио-кнопки выбора */}
<Radio.Group
value={docChoice}
onChange={(e) => {
setDocChoice(e.target.value);
if (e.target.value === 'none') {
setCurrentUploadedFiles([]);
}
}}
style={{ marginBottom: 16, display: 'block' }}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Radio value="upload" style={{ fontSize: 16 }}>
📎 Загрузить документ
</Radio>
<Radio value="none" style={{ fontSize: 16 }}>
У меня нет этого документа
</Radio>
</Space>
</Radio.Group>
{/* Загрузка файлов — показываем только если выбрано "Загрузить" */}
{docChoice === 'upload' && (
<Dragger
multiple={true}
beforeUpload={() => false}
fileList={currentUploadedFiles}
onChange={({ fileList }) => handleFilesChange(fileList)}
onRemove={(file) => {
setCurrentUploadedFiles(prev => prev.filter(f => f.uid !== file.uid));
return true;
}}
accept={currentDoc.accept?.map((ext: string) => `.${ext}`).join(',') || '.pdf,.jpg,.jpeg,.png'}
disabled={submitting}
style={{ marginBottom: 24 }}
>
<p className="ant-upload-drag-icon">
<InboxOutlined style={{ color: '#595959', fontSize: 32 }} />
</p>
<p className="ant-upload-text">
Перетащите файлы или нажмите для выбора
</p>
<p className="ant-upload-hint">
📌 Можно загрузить несколько файлов (все страницы документа)
<br />
Форматы: {currentDoc.accept?.join(', ') || 'PDF, JPG, PNG'} (до 20 МБ каждый)
</p>
</Dragger>
)}
{/* Предупреждение если "нет документа" для важного */}
{docChoice === 'none' && currentDoc.required && (
<div style={{
padding: 12,
background: '#fff7e6',
border: '1px solid #ffd591',
borderRadius: 8,
marginBottom: 16
}}>
<Text type="warning">
Этот документ важен для рассмотрения заявки. Постарайтесь найти его позже.
</Text>
</div>
)}
{/* Кнопки */}
<Space style={{ marginTop: 16 }}>
<Button onClick={onPrev}> Назад</Button>
<Button
type="primary"
onClick={handleDocContinue}
disabled={!canContinue || submitting}
loading={submitting}
>
{submitting ? 'Загружаем...' : 'Продолжить →'}
</Button>
</Space>
{/* Уже загруженные */}
{uploadedDocs.length > 0 && (
<div style={{ marginTop: 24, padding: 12, background: '#f6ffed', borderRadius: 8 }}>
<Text strong> Загружено:</Text>
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
{/* Убираем дубликаты и используем уникальные ключи */}
{Array.from(new Set(uploadedDocs)).map((docId, idx) => {
const doc = documentsRequired.find((d: any) => d.id === docId);
return <li key={`${docId}_${idx}`}>{doc?.name || docId}</li>;
})}
</ul>
</div>
)}
</div>
)}
{/* ✅ НОВЫЙ ФЛОУ: Все документы загружены */}
{hasNewFlowDocs && allDocsProcessed && (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Title level={4}> Все документы загружены!</Title>
<Paragraph type="secondary">
Загружено: {uploadedDocs.length}, пропущено: {skippedDocs.length}
</Paragraph>
<Button type="primary" size="large" onClick={handleAllDocsComplete}>
Продолжить
</Button>
</div>
)}
{/* СТАРЫЙ ФЛОУ: Ожидание визарда */}
{!hasNewFlowDocs && isWaiting && (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<img
src={AiWorkingIllustration}
@@ -1306,7 +1707,8 @@ export default function StepWizardPlan({
</div>
)}
{!isWaiting && plan && (
{/* СТАРЫЙ ФЛОУ: Визард готов */}
{!hasNewFlowDocs && !isWaiting && plan && (
<div>
<Title level={4} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<ThunderboltOutlined style={{ color: '#595959' }} /> План действий
@@ -1316,41 +1718,60 @@ export default function StepWizardPlan({
</Paragraph>
{documents.length > 0 && (
<Card
size="small"
style={{
borderRadius: 8,
background: '#fff',
border: '1px solid #d9d9d9',
marginBottom: 24,
}}
title="Документы, которые понадобятся"
>
<Space direction="vertical" style={{ width: '100%' }}>
{documents.map((doc: any) => (
<div
key={doc.id}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
flexWrap: 'wrap',
}}
>
<div>
<Text strong>{doc.name}</Text>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
{doc.hints}
</Paragraph>
<>
<Card
size="small"
style={{
borderRadius: 8,
background: '#fff',
border: '1px solid #d9d9d9',
marginBottom: 24,
}}
title="Документы, которые понадобятся"
>
<Space direction="vertical" style={{ width: '100%' }}>
{documents.map((doc: any) => (
<div
key={doc.id}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
flexWrap: 'wrap',
}}
>
<div>
<Text strong>{doc.name}</Text>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
{doc.hints}
</Paragraph>
</div>
<Tag color={doc.required ? 'volcano' : 'geekblue'}>
{doc.required ? 'Обязательно' : 'Опционально'}
</Tag>
</div>
<Tag color={doc.required ? 'volcano' : 'geekblue'}>
{doc.required ? 'Обязательно' : 'Опционально'}
</Tag>
</div>
))}
</Space>
</Card>
))}
</Space>
</Card>
{/* Блоки загрузки для каждого документа из плана */}
<div style={{ marginTop: 16, marginBottom: 24 }}>
<Text strong style={{ fontSize: 16, marginBottom: 16, display: 'block' }}>
Загрузите документы
</Text>
<Space direction="vertical" style={{ width: '100%' }}>
{documents.map((doc: any) => {
const docKey = doc.id || doc.name || `doc_${Math.random()}`;
return (
<div key={docKey}>
{renderDocumentBlocks(docKey, [doc])}
</div>
);
})}
</Space>
</div>
</>
)}
{renderQuestions()}
@@ -1360,6 +1781,3 @@ export default function StepWizardPlan({
</div>
);
}