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

- Исправлена ошибка порядка объявления allDocsProcessed (Cannot access before initialization)
- Исправлена логика поиска незагруженного документа: поиск с начала, если сохранённый индекс уже обработан
- Исправлен расчёт прогресса: теперь используется количество обработанных документов (uploadedDocs + skippedDocs), а не currentDocIndex
- Убрана синхронизация currentDocIndex из formData, которая перезаписывала правильный индекс
- Добавлена логика автоматического пропуска уже загруженных документов при открытии формы
- Добавлено подробное логирование для отладки состояния документов
- Исправлена логика определения завершённости: проверяется каждый документ из documentsRequired

Результат:
- Форма корректно показывает следующий незагруженный документ
- Прогресс правильно отображает процент обработанных документов (75% при 3 из 4)
- Система не требует повторной загрузки уже загруженных документов
This commit is contained in:
AI Assistant
2025-11-27 14:36:42 +03:00
parent 02689e65db
commit 64385c430d

View File

@@ -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<Set<string>>(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) && (
<Button
type="link"
danger
size="small"
onClick={() => removeDocumentBlock(docId, block.id)}
>
Удалить
</Button>
<Button
type="link"
danger
size="small"
onClick={() => removeDocumentBlock(docId, block.id)}
>
Удалить
</Button>
)
}
>
@@ -1064,28 +1064,28 @@ export default function StepWizardPlan({
{/* Поле описания показываем только для дополнительных блоков (idx > 0)
или для общих документов (docs_exist) */}
{(idx > 0 || !isPredefinedDoc) && (
<Input
<Input
placeholder="Уточните тип документа (например: Претензия от 12.05)"
value={block.description}
onChange={(e) =>
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 && (
<Select
value={block.category || docId}
onChange={(value) => updateDocumentBlock(docId, block.id, { category: value })}
placeholder="Категория блока"
>
{documentCategoryOptions.map((option) => (
<Option key={`${docId}-${option.value}`} value={option.value}>
{option.label}
</Option>
))}
</Select>
<Select
value={block.category || docId}
onChange={(value) => updateDocumentBlock(docId, block.id, { category: value })}
placeholder="Категория блока"
>
{documentCategoryOptions.map((option) => (
<Option key={`${docId}-${option.value}`} value={option.value}>
{option.label}
</Option>
))}
</Select>
)}
<Dragger
@@ -1129,15 +1129,15 @@ export default function StepWizardPlan({
))}
{/* Кнопка "Добавить" только если документ не пропущен */}
{!isSkipped && (!isPredefinedDoc || currentBlocks.length === 0) && (
<Button
icon={<PlusOutlined />}
<Button
icon={<PlusOutlined />}
onClick={() => addDocumentBlock(docId, docLabel, docList)}
style={{ width: '100%' }}
>
style={{ width: '100%' }}
>
{isPredefinedDoc && currentBlocks.length === 0
? `Загрузить ${singleDocName || docLabel}`
: `Добавить документы (${docLabel})`}
</Button>
</Button>
)}
</Space>
);
@@ -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 (
<>
<Form.Item
label={question.label}
name={question.name}
rules={[
{
required: question.required,
message: 'Поле обязательно для заполнения',
},
]}
>
{renderQuestionField(question)}
</Form.Item>
{questionDocs.length > 0 && isAffirmative(questionValue) && (
<div style={{ marginBottom: 24 }}>
<Text strong>Загрузите документы:</Text>
return (
<>
<Form.Item
label={question.label}
name={question.name}
rules={[
{
required: question.required,
message: 'Поле обязательно для заполнения',
},
]}
>
{renderQuestionField(question)}
</Form.Item>
{questionDocs.length > 0 && isAffirmative(questionValue) && (
<div style={{ marginBottom: 24 }}>
<Text strong>Загрузите документы:</Text>
<Space direction="vertical" style={{ width: '100%', marginTop: 16 }}>
{questionDocs.map((doc) => {
// Используем doc.id как ключ для отдельного хранения блоков каждого документа
@@ -1336,12 +1336,12 @@ export default function StepWizardPlan({
);
})}
</Space>
</div>
)}
</>
);
}}
</Form.Item>
</div>
)}
</>
);
}}
</Form.Item>
);
})}
@@ -1354,7 +1354,7 @@ export default function StepWizardPlan({
</Form>
{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<string[]>(Array.from(new Set(initialUploadedDocs)));
const [skippedDocs, setSkippedDocs] = useState<string[]>(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<any[]>([]); // Массив загруженных файлов
// Текущий документ для загрузки
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 ? (
<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>
<Text type="secondary">{Math.round(((uploadedDocs.length + skippedDocs.length) / documentsRequired.length) * 100)}% завершено</Text>
</div>
<Progress
percent={Math.round((currentDocIndex / documentsRequired.length) * 100)}
percent={Math.round(((uploadedDocs.length + skippedDocs.length) / documentsRequired.length) * 100)}
showInfo={false}
strokeColor="#595959"
/>
@@ -1659,20 +1858,52 @@ export default function StepWizardPlan({
</div>
)}
</div>
)}
{/* ✅ НОВЫЙ ФЛОУ: Все документы загружены */}
{hasNewFlowDocs && allDocsProcessed && (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Title level={4}> Все документы загружены!</Title>
<Paragraph type="secondary">
) : hasNewFlowDocs && !allDocsProcessed && currentDocIndex >= documentsRequired.length ? (
<div style={{ padding: '24px 0', textAlign: 'center' }}>
<Text type="warning">
Ошибка: индекс документа ({currentDocIndex}) выходит за границы массива ({documentsRequired.length}).
<br />
Загружено: {uploadedDocs.length}, пропущено: {skippedDocs.length}
</Paragraph>
<Button type="primary" size="large" onClick={handleAllDocsComplete}>
Продолжить
</Text>
<Button
type="primary"
style={{ marginTop: 16 }}
onClick={() => {
const nextIndex = findFirstUnprocessedDoc(0);
setCurrentDocIndex(nextIndex);
updateFormData({ current_doc_index: nextIndex });
}}
>
Исправить и продолжить
</Button>
</div>
)}
) : 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 (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Title level={4}> Все документы обработаны!</Title>
<Paragraph type="secondary">
Загружено: {uploadedCount} из {documentsRequired.length}, пропущено: {skippedCount}
</Paragraph>
<Button type="primary" size="large" onClick={handleAllDocsComplete}>
Продолжить
</Button>
</div>
);
})()}
{/* СТАРЫЙ ФЛОУ: Ожидание визарда */}
{!hasNewFlowDocs && isWaiting && (
@@ -1719,41 +1950,41 @@ export default function StepWizardPlan({
{documents.length > 0 && (
<>
<Card
size="small"
style={{
<Card
size="small"
style={{
borderRadius: 8,
background: '#fff',
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>
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>
))}
</Space>
</Card>
<Tag color={doc.required ? 'volcano' : 'geekblue'}>
{doc.required ? 'Обязательно' : 'Опционально'}
</Tag>
</div>
))}
</Space>
</Card>
{/* Блоки загрузки для каждого документа из плана */}
<div style={{ marginTop: 16, marginBottom: 24 }}>