diff --git a/frontend/src/components/form/StepWizardPlan.tsx b/frontend/src/components/form/StepWizardPlan.tsx index 8c6a67b..66e76b7 100644 --- a/frontend/src/components/form/StepWizardPlan.tsx +++ b/frontend/src/components/form/StepWizardPlan.tsx @@ -249,15 +249,15 @@ export default function StepWizardPlan({ : ''; return [ - ...blocks, - { - id: generateBlockId(docId), - fieldName: docId, + ...blocks, + { + id: generateBlockId(docId), + fieldName: docId, description: autoDescription, category: category, - docLabel: docLabel, - files: [], - }, + docLabel: docLabel, + files: [], + }, ]; }); }; @@ -341,7 +341,7 @@ export default function StepWizardPlan({ // Автоматически создаём блоки для ВСЕХ документов из плана при загрузке // Используем ref чтобы отслеживать какие блоки уже созданы const createdDocBlocksRef = useRef>(new Set()); - + useEffect(() => { if (!plan || !documents || documents.length === 0) return; @@ -495,7 +495,7 @@ export default function StepWizardPlan({ // TODO: onNext() для перехода к StepDocumentsNew return; } - + const wizardPayload = extractWizardPayload(payload); const hasWizardPlan = Boolean(wizardPayload); @@ -602,14 +602,14 @@ export default function StepWizardPlan({ // Для обязательных документов описание не требуется // Для предопределённых документов описание не требуется if (!doc.required && !isPredefinedDoc) { - const missingDescription = blocks.some( - (block) => block.files.length > 0 && !block.description?.trim() - ); - if (missingDescription) { + const missingDescription = blocks.some( + (block) => block.files.length > 0 && !block.description?.trim() + ); + if (missingDescription) { return `Заполните описание для документа "${doc.name}"`; - } } } + } const customMissingDescription = customFileBlocks.some( (block) => block.files.length > 0 && !block.description?.trim() @@ -737,8 +737,8 @@ export default function StepWizardPlan({ } if (cat.includes('payment') || cat.includes('cheque') || cat.includes('receipt') || cat.includes('подтверждение') || cat === 'payment_proof') { - return 'upload_payment'; - } + return 'upload_payment'; + } if (cat.includes('correspondence') || cat.includes('chat') || cat.includes('переписка')) { return 'upload_correspondence'; } @@ -906,7 +906,7 @@ export default function StepWizardPlan({ eventSourceRef.current = null; // Переходим к следующему шагу (форма подтверждения) - onNext(); + onNext(); } else if (data.event_type === 'claim_plan_error' || data.status === 'error') { message.destroy(); message.error(data.message || 'Ошибка получения данных заявления'); @@ -1049,14 +1049,14 @@ export default function StepWizardPlan({ // Кнопка "Удалить" только если это дополнительный блок (idx > 0) // Первый блок предустановленного документа удалять нельзя (currentBlocks.length > 1 && idx > 0) && ( - + ) } > @@ -1064,28 +1064,28 @@ export default function StepWizardPlan({ {/* Поле описания показываем только для дополнительных блоков (idx > 0) или для общих документов (docs_exist) */} {(idx > 0 || !isPredefinedDoc) && ( - - updateDocumentBlock(docId, block.id, { description: e.target.value }) - } - /> + value={block.description} + onChange={(e) => + updateDocumentBlock(docId, block.id, { description: e.target.value }) + } + /> )} {/* Выпадашка категорий только для общих вопросов (docs_exist, correspondence_exist) */} {!isPredefinedDoc && ( - + )} } + + )} ); @@ -1258,14 +1258,14 @@ export default function StepWizardPlan({ return prev[question.ask_if!.field] !== curr[question.ask_if!.field]; } : true} // ✅ Для безусловных полей shouldUpdate=true, чтобы render function работала > - {() => { - const values = form.getFieldsValue(true); - if (!evaluateCondition(question.ask_if, values)) { + {() => { + const values = form.getFieldsValue(true); + if (!evaluateCondition(question.ask_if, values)) { console.log(`⏭️ Question ${question.name} skipped: condition not met`, question.ask_if, values); - return null; - } - const questionDocs = documentGroups[question.name] || []; - const questionValue = values?.[question.name]; + return null; + } + const questionDocs = documentGroups[question.name] || []; + const questionValue = values?.[question.name]; // Скрываем вопросы, которые связаны с загрузкой документов // Если в плане визарда есть документы, не показываем поля про загрузку (text/textarea/file) @@ -1308,23 +1308,23 @@ export default function StepWizardPlan({ console.log(`✅ Question ${question.name} will render:`, { input_type: question.input_type, label: question.label, required: question.required }); - return ( - <> - - {renderQuestionField(question)} - - {questionDocs.length > 0 && isAffirmative(questionValue) && ( -
- Загрузите документы: + return ( + <> + + {renderQuestionField(question)} + + {questionDocs.length > 0 && isAffirmative(questionValue) && ( +
+ Загрузите документы: {questionDocs.map((doc) => { // Используем doc.id как ключ для отдельного хранения блоков каждого документа @@ -1336,12 +1336,12 @@ export default function StepWizardPlan({ ); })} -
- )} - - ); - }} - +
+ )} + + ); + }} + ); })} @@ -1354,7 +1354,7 @@ export default function StepWizardPlan({ {renderCustomUploads()} - ); + ); }; if (!formData.session_id) { @@ -1383,18 +1383,191 @@ export default function StepWizardPlan({ }); // Состояние для поэкранной загрузки документов (новый флоу) - 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(Array.from(new Set(initialUploadedDocs))); const [skippedDocs, setSkippedDocs] = useState(formData.documents_skipped || []); + + // Отладка: логируем инициализацию + useEffect(() => { + console.log('🔍 Инициализация документов:', { + documentsRequiredCount: documentsRequired.length, + initialUploadedDocs, + uploadedDocs, + skippedDocs, + formDataCurrentDocIndex: formData.current_doc_index, + }); + }, []); // Только при первой загрузке + + // Находим первый незагруженный документ при инициализации + const findFirstUnprocessedDoc = useCallback((startIndex: number = 0) => { + for (let i = startIndex; i < documentsRequired.length; i++) { + const doc = documentsRequired[i]; + const docId = doc.id || doc.name; + if (!uploadedDocs.includes(docId) && !skippedDocs.includes(docId)) { + return i; + } + } + return documentsRequired.length; // Все документы обработаны + }, [documentsRequired, uploadedDocs, skippedDocs]); + + const [currentDocIndex, setCurrentDocIndex] = useState(() => { + const savedIndex = formData.current_doc_index || 0; + // Используем initialUploadedDocs и formData.documents_skipped для инициализации + const initUploaded = Array.from(new Set(initialUploadedDocs)); + const initSkipped = formData.documents_skipped || []; + + // Находим первый незагруженный документ + // Сначала проверяем с сохранённого индекса, потом с начала + let firstUnprocessed = documentsRequired.length; // По умолчанию - все обработаны + + // Проверяем с сохранённого индекса до конца + for (let i = savedIndex; i < documentsRequired.length; i++) { + const doc = documentsRequired[i]; + const docId = doc.id || doc.name; + if (!initUploaded.includes(docId) && !initSkipped.includes(docId)) { + firstUnprocessed = i; + break; + } + } + + // Если не нашли с сохранённого индекса, проверяем с начала до сохранённого + if (firstUnprocessed === documentsRequired.length) { + for (let i = 0; i < savedIndex; i++) { + const doc = documentsRequired[i]; + const docId = doc.id || doc.name; + if (!initUploaded.includes(docId) && !initSkipped.includes(docId)) { + firstUnprocessed = i; + break; + } + } + } + + console.log('🔍 Инициализация currentDocIndex:', { + savedIndex, + firstUnprocessed, + documentsRequiredLength: documentsRequired.length, + initUploaded, + initSkipped, + documentsRequiredList: documentsRequired.map((d: any) => ({ + id: d.id || d.name, + name: d.name, + uploaded: initUploaded.includes(d.id || d.name), + skipped: initSkipped.includes(d.id || d.name), + })), + }); + // Убеждаемся, что индекс не выходит за границы + return Math.min(firstUnprocessed, documentsRequired.length); + }); + + // Исправляем currentDocIndex только если он выходит за границы или указывает на уже обработанный документ + useEffect(() => { + if (documentsRequired.length === 0) return; + + // Если текущий индекс выходит за границы, исправляем + if (currentDocIndex >= documentsRequired.length) { + const firstUnprocessed = findFirstUnprocessedDoc(0); + console.log('🔄 Исправление currentDocIndex (выход за границы):', { + currentIndex: currentDocIndex, + documentsRequiredLength: documentsRequired.length, + firstUnprocessed, + }); + setCurrentDocIndex(firstUnprocessed); + updateFormData({ current_doc_index: firstUnprocessed }); + return; + } + + // Если текущий документ уже обработан, переходим к следующему + const currentDoc = documentsRequired[currentDocIndex]; + if (currentDoc) { + const docId = currentDoc.id || currentDoc.name; + if (uploadedDocs.includes(docId) || skippedDocs.includes(docId)) { + const firstUnprocessed = findFirstUnprocessedDoc(currentDocIndex + 1); + console.log('🔄 Исправление currentDocIndex (документ уже обработан):', { + currentIndex: currentDocIndex, + currentDocId: docId, + firstUnprocessed, + }); + setCurrentDocIndex(firstUnprocessed); + updateFormData({ current_doc_index: firstUnprocessed }); + } + } + }, [currentDocIndex, documentsRequired.length, uploadedDocs, skippedDocs, findFirstUnprocessedDoc, updateFormData]); + const [docChoice, setDocChoice] = useState<'upload' | 'none'>('upload'); // Выбор: загрузить или нет документа (по умолчанию - загрузить) const [currentUploadedFiles, setCurrentUploadedFiles] = useState([]); // Массив загруженных файлов // Текущий документ для загрузки const currentDoc = documentsRequired[currentDocIndex]; const isLastDoc = currentDocIndex >= documentsRequired.length - 1; - const allDocsProcessed = currentDocIndex >= documentsRequired.length; + + // Проверяем, что ВСЕ документы либо загружены, либо пропущены + const allDocsProcessed = useMemo(() => { + // Проверяем каждый документ из documentsRequired + const allRequiredDocsProcessed = documentsRequired.every((doc: any) => { + const docId = doc.id || doc.name; + return uploadedDocs.includes(docId) || skippedDocs.includes(docId); + }); + + const processedCount = uploadedDocs.length + skippedDocs.length; + const allProcessed = allRequiredDocsProcessed && processedCount >= documentsRequired.length; + + console.log('🔍 Проверка завершённости:', { + uploadedDocs: uploadedDocs.length, + skippedDocs: skippedDocs.length, + totalRequired: documentsRequired.length, + processedCount, + allRequiredDocsProcessed, + allProcessed, + uploadedDocsList: uploadedDocs, + skippedDocsList: skippedDocs, + requiredDocsList: documentsRequired.map((d: any) => { + const docId = d.id || d.name; + return { + id: docId, + name: d.name, + uploaded: uploadedDocs.includes(docId), + skipped: skippedDocs.includes(docId), + }; + }), + }); + + return allProcessed; + }, [uploadedDocs, skippedDocs, documentsRequired]); + + // Отладка: логируем состояние текущего документа + useEffect(() => { + console.log('🔍 Текущий документ для загрузки:', { + currentDocIndex, + documentsRequiredLength: documentsRequired.length, + currentDoc: currentDoc ? { id: currentDoc.id, name: currentDoc.name } : null, + uploadedDocs, + skippedDocs, + allDocsProcessed, + }); + }, [currentDocIndex, documentsRequired.length, currentDoc, uploadedDocs, skippedDocs, allDocsProcessed]); + + // Автоматически пропускаем уже загруженные документы + useEffect(() => { + if (!currentDoc || allDocsProcessed) return; + + const docId = currentDoc.id || currentDoc.name; + const isAlreadyUploaded = uploadedDocs.includes(docId); + const isAlreadySkipped = skippedDocs.includes(docId); + + if (isAlreadyUploaded || isAlreadySkipped) { + console.log(`⏭️ Документ "${currentDoc.name}" уже обработан, переходим к следующему`); + const nextIndex = findFirstUnprocessedDoc(currentDocIndex + 1); + + // Обновляем только если следующий индекс отличается от текущего + if (nextIndex !== currentDocIndex) { + setCurrentDocIndex(nextIndex); + updateFormData({ + current_doc_index: nextIndex, + }); + } + } + }, [currentDoc, uploadedDocs, skippedDocs, currentDocIndex, allDocsProcessed, findFirstUnprocessedDoc, updateFormData]); // Обработчик выбора файлов (НЕ отправляем сразу, только сохраняем) const handleFilesChange = (fileList: any[]) => { @@ -1418,13 +1591,26 @@ export default function StepWizardPlan({ const newSkipped = [...skippedDocs, currentDoc.id]; setSkippedDocs(newSkipped); + // Находим следующий незагруженный документ (используем обновлённый список) + const findNextUnprocessed = (startIndex: number) => { + for (let i = startIndex; i < documentsRequired.length; i++) { + const doc = documentsRequired[i]; + const docId = doc.id || doc.name; + if (!uploadedDocs.includes(docId) && !newSkipped.includes(docId)) { + return i; + } + } + return documentsRequired.length; + }; + const nextIndex = findNextUnprocessed(currentDocIndex + 1); + updateFormData({ documents_skipped: newSkipped, - current_doc_index: currentDocIndex + 1, + current_doc_index: nextIndex, }); - // Переход к следующему (сброс состояния в useEffect) - setCurrentDocIndex(prev => prev + 1); + // Переход к следующему незагруженному документу + setCurrentDocIndex(nextIndex); return; } @@ -1488,13 +1674,26 @@ export default function StepWizardPlan({ : [...uploadedDocs, currentDoc.id]; setUploadedDocs(newUploaded); + // Находим следующий незагруженный документ (используем обновлённый список) + const findNextUnprocessed = (startIndex: number) => { + for (let i = startIndex; i < documentsRequired.length; i++) { + const doc = documentsRequired[i]; + const docId = doc.id || doc.name; + if (!newUploaded.includes(docId) && !skippedDocs.includes(docId)) { + return i; + } + } + return documentsRequired.length; + }; + const nextIndex = findNextUnprocessed(currentDocIndex + 1); + updateFormData({ documents_uploaded: uploadedDocsData, - current_doc_index: currentDocIndex + 1, + current_doc_index: nextIndex, }); - // Переход к следующему (сброс состояния в useEffect) - setCurrentDocIndex(prev => prev + 1); + // Переход к следующему незагруженному документу + setCurrentDocIndex(nextIndex); } catch (error: any) { message.error(`Ошибка загрузки: ${error.message}`); @@ -1540,16 +1739,16 @@ export default function StepWizardPlan({ }} > {/* ✅ НОВЫЙ ФЛОУ: Поэкранная загрузка документов */} - {hasNewFlowDocs && !allDocsProcessed && currentDoc && ( + {hasNewFlowDocs && !allDocsProcessed && currentDocIndex < documentsRequired.length && currentDoc ? (
{/* Прогресс */}
Документ {currentDocIndex + 1} из {documentsRequired.length} - {Math.round((currentDocIndex / documentsRequired.length) * 100)}% завершено + {Math.round(((uploadedDocs.length + skippedDocs.length) / documentsRequired.length) * 100)}% завершено
@@ -1659,20 +1858,52 @@ export default function StepWizardPlan({
)}
- )} - - {/* ✅ НОВЫЙ ФЛОУ: Все документы загружены */} - {hasNewFlowDocs && allDocsProcessed && ( -
- ✅ Все документы загружены! - + ) : hasNewFlowDocs && !allDocsProcessed && currentDocIndex >= documentsRequired.length ? ( +
+ + ⚠️ Ошибка: индекс документа ({currentDocIndex}) выходит за границы массива ({documentsRequired.length}). +
Загружено: {uploadedDocs.length}, пропущено: {skippedDocs.length} - -
- )} + ) : null} + + {/* ✅ НОВЫЙ ФЛОУ: Все документы загружены */} + {hasNewFlowDocs && allDocsProcessed && (() => { + // Правильно считаем загруженные и пропущенные документы из documentsRequired + const uploadedCount = documentsRequired.filter((doc: any) => { + const docId = doc.id || doc.name; + return uploadedDocs.includes(docId); + }).length; + + const skippedCount = documentsRequired.filter((doc: any) => { + const docId = doc.id || doc.name; + return skippedDocs.includes(docId); + }).length; + + return ( +
+ ✅ Все документы обработаны! + + Загружено: {uploadedCount} из {documentsRequired.length}, пропущено: {skippedCount} + + +
+ ); + })()} {/* СТАРЫЙ ФЛОУ: Ожидание визарда */} {!hasNewFlowDocs && isWaiting && ( @@ -1719,41 +1950,41 @@ export default function StepWizardPlan({ {documents.length > 0 && ( <> - - - {documents.map((doc: any) => ( -
-
- {doc.name} - - {doc.hints} - -
- - {doc.required ? 'Обязательно' : 'Опционально'} - + marginBottom: 24, + }} + title="Документы, которые понадобятся" + > + + {documents.map((doc: any) => ( +
+
+ {doc.name} + + {doc.hints} +
- ))} - - + + {doc.required ? 'Обязательно' : 'Опционально'} + +
+ ))} +
+ {/* Блоки загрузки для каждого документа из плана */}