283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { Loader2, CheckCircle2, XCircle, AlertTriangle, RefreshCw, Eye, FileText } from 'lucide-react';
|
|||
|
|
import { ProcessingJob, ProcessingError } from '../../types';
|
|||
|
|
|
|||
|
|
interface ReportProcessingProps {
|
|||
|
|
jobId: string;
|
|||
|
|
reportId?: number;
|
|||
|
|
onComplete?: () => void;
|
|||
|
|
onViewReports?: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const ReportProcessing: React.FC<ReportProcessingProps> = ({
|
|||
|
|
jobId,
|
|||
|
|
reportId,
|
|||
|
|
onComplete,
|
|||
|
|
onViewReports
|
|||
|
|
}) => {
|
|||
|
|
const [jobStatus, setJobStatus] = useState<ProcessingJob | null>(null);
|
|||
|
|
const [isPolling, setIsPolling] = useState(true);
|
|||
|
|
const [showPreview, setShowPreview] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!jobId || !isPolling) return;
|
|||
|
|
|
|||
|
|
const fetchStatus = async () => {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`/api/finance/processing-status/${jobId}`);
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error('Не удалось получить статус обработки');
|
|||
|
|
}
|
|||
|
|
const status = await response.json();
|
|||
|
|
setJobStatus(status);
|
|||
|
|
|
|||
|
|
// Если обработка завершена, останавливаем опрос
|
|||
|
|
if (status.status === 'completed' || status.status === 'failed') {
|
|||
|
|
setIsPolling(false);
|
|||
|
|
if (onComplete) {
|
|||
|
|
onComplete();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Ошибка получения статуса:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Первый запрос сразу
|
|||
|
|
fetchStatus();
|
|||
|
|
|
|||
|
|
// Затем опрашиваем каждые 2 секунды
|
|||
|
|
const interval = setInterval(fetchStatus, 2000);
|
|||
|
|
|
|||
|
|
return () => clearInterval(interval);
|
|||
|
|
}, [jobId, isPolling, onComplete]);
|
|||
|
|
|
|||
|
|
const handleRetry = () => {
|
|||
|
|
setIsPolling(true);
|
|||
|
|
setShowPreview(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getStatusIcon = () => {
|
|||
|
|
if (!jobStatus) return <Loader2 className="w-6 h-6 animate-spin text-primary-600" />;
|
|||
|
|
|
|||
|
|
switch (jobStatus.status) {
|
|||
|
|
case 'processing':
|
|||
|
|
return <Loader2 className="w-6 h-6 animate-spin text-primary-600" />;
|
|||
|
|
case 'completed':
|
|||
|
|
return <CheckCircle2 className="w-6 h-6 text-emerald-600" />;
|
|||
|
|
case 'failed':
|
|||
|
|
return <XCircle className="w-6 h-6 text-red-600" />;
|
|||
|
|
default:
|
|||
|
|
return <Loader2 className="w-6 h-6 animate-spin text-slate-400" />;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getStatusText = () => {
|
|||
|
|
if (!jobStatus) return 'Ожидание...';
|
|||
|
|
|
|||
|
|
switch (jobStatus.status) {
|
|||
|
|
case 'processing':
|
|||
|
|
return 'Обработка файла';
|
|||
|
|
case 'completed':
|
|||
|
|
return 'Обработка завершена успешно';
|
|||
|
|
case 'failed':
|
|||
|
|
return 'Обработка завершена с ошибками';
|
|||
|
|
default:
|
|||
|
|
return 'Ожидание...';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!jobStatus) {
|
|||
|
|
return (
|
|||
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
|||
|
|
<div className="flex items-center justify-center gap-3 py-8">
|
|||
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary-600" />
|
|||
|
|
<span className="text-slate-600">Загрузка статуса обработки...</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6 animate-fade-in">
|
|||
|
|
{/* Основной статус */}
|
|||
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
|||
|
|
<div className="flex items-center justify-between mb-6">
|
|||
|
|
<h3 className="text-lg font-bold text-slate-800">Обработка отчета</h3>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
{getStatusIcon()}
|
|||
|
|
<span className="text-sm font-medium text-slate-700">{getStatusText()}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Прогресс-бар */}
|
|||
|
|
<div className="mb-6">
|
|||
|
|
<div className="flex justify-between items-center mb-2">
|
|||
|
|
<span className="text-xs font-medium text-slate-600">Прогресс</span>
|
|||
|
|
<span className="text-xs font-bold text-primary-600">{jobStatus.progress}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-full bg-slate-200 rounded-full h-3 overflow-hidden">
|
|||
|
|
<div
|
|||
|
|
className="bg-primary-600 h-full transition-all duration-300 rounded-full"
|
|||
|
|
style={{ width: `${jobStatus.progress}%` }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Текущий этап */}
|
|||
|
|
{jobStatus.currentStep && (
|
|||
|
|
<div className="mb-6 p-3 bg-slate-50 rounded-lg">
|
|||
|
|
<p className="text-sm text-slate-700">
|
|||
|
|
<strong>Текущий этап:</strong> {jobStatus.currentStep}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Результаты */}
|
|||
|
|
{jobStatus.result && (
|
|||
|
|
<div className="mb-6 p-4 bg-emerald-50 rounded-lg border border-emerald-200">
|
|||
|
|
<h4 className="font-bold text-emerald-800 text-sm mb-2">Результаты обработки:</h4>
|
|||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-slate-600">Всего строк</p>
|
|||
|
|
<p className="font-bold text-slate-800">{jobStatus.result.totalRows}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-slate-600">Обработано</p>
|
|||
|
|
<p className="font-bold text-emerald-600">{jobStatus.result.processedRows}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-slate-600">Ошибок</p>
|
|||
|
|
<p className="font-bold text-red-600">{jobStatus.result.errorRows}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-slate-600">Домов найдено</p>
|
|||
|
|
<p className="font-bold text-primary-600">{jobStatus.result.buildingsFound || 0}</p>
|
|||
|
|
</div>
|
|||
|
|
{jobStatus.result.buildingsCreated > 0 && (
|
|||
|
|
<div>
|
|||
|
|
<p className="text-slate-600">Домов создано</p>
|
|||
|
|
<p className="font-bold text-emerald-600">{jobStatus.result.buildingsCreated}</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Предпросмотр результатов */}
|
|||
|
|
{jobStatus.status === 'completed' && jobStatus.result && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowPreview(!showPreview)}
|
|||
|
|
className="w-full flex items-center justify-center gap-2 p-3 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors text-sm font-medium text-slate-700"
|
|||
|
|
>
|
|||
|
|
<Eye className="w-4 h-4" />
|
|||
|
|
{showPreview ? 'Скрыть' : 'Показать'} предпросмотр данных
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Кнопка повтора при ошибках */}
|
|||
|
|
{jobStatus.status === 'failed' && (
|
|||
|
|
<button
|
|||
|
|
onClick={handleRetry}
|
|||
|
|
className="w-full flex items-center justify-center gap-2 p-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-bold"
|
|||
|
|
>
|
|||
|
|
<RefreshCw className="w-4 h-4" />
|
|||
|
|
Повторить обработку
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Кнопка просмотра отчетов после успешной обработки */}
|
|||
|
|
{jobStatus.status === 'completed' && onViewReports && (
|
|||
|
|
<button
|
|||
|
|
onClick={onViewReports}
|
|||
|
|
className="w-full flex items-center justify-center gap-2 p-3 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors text-sm font-bold"
|
|||
|
|
>
|
|||
|
|
<FileText className="w-4 h-4" />
|
|||
|
|
Просмотреть загруженные отчеты
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Ошибки */}
|
|||
|
|
{jobStatus.errors && jobStatus.errors.length > 0 && (
|
|||
|
|
<div className="bg-white rounded-2xl border border-red-200 shadow-sm p-6">
|
|||
|
|
<div className="flex items-center gap-2 mb-4">
|
|||
|
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
|||
|
|
<h3 className="text-lg font-bold text-red-800">
|
|||
|
|
Ошибки обработки ({jobStatus.errors.length})
|
|||
|
|
</h3>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|||
|
|
{jobStatus.errors.map((error: ProcessingError, index: number) => (
|
|||
|
|
<div
|
|||
|
|
key={index}
|
|||
|
|
className="p-4 bg-red-50 rounded-lg border border-red-200"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-start justify-between mb-2">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<span className="text-xs font-bold text-red-600">
|
|||
|
|
Строка {error.row}
|
|||
|
|
</span>
|
|||
|
|
{error.column && (
|
|||
|
|
<span className="text-xs text-slate-500">
|
|||
|
|
Колонка: {error.column}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-sm text-red-800 font-medium mb-2">{error.message}</p>
|
|||
|
|
{error.suggestion && (
|
|||
|
|
<p className="text-xs text-slate-600 italic">
|
|||
|
|
💡 {error.suggestion}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
{error.data && (
|
|||
|
|
<details className="mt-2">
|
|||
|
|
<summary className="text-xs text-slate-500 cursor-pointer">
|
|||
|
|
Показать данные строки
|
|||
|
|
</summary>
|
|||
|
|
<pre className="mt-2 p-2 bg-slate-100 rounded text-xs overflow-x-auto">
|
|||
|
|
{JSON.stringify(error.data, null, 2)}
|
|||
|
|
</pre>
|
|||
|
|
</details>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Предупреждения */}
|
|||
|
|
{jobStatus.warnings && jobStatus.warnings.length > 0 && (
|
|||
|
|
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-6">
|
|||
|
|
<div className="flex items-center gap-2 mb-4">
|
|||
|
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
|||
|
|
<h3 className="text-lg font-bold text-amber-800">
|
|||
|
|
Предупреждения ({jobStatus.warnings.length})
|
|||
|
|
</h3>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{jobStatus.warnings.map((warning: any, index: number) => (
|
|||
|
|
<div
|
|||
|
|
key={index}
|
|||
|
|
className="p-3 bg-amber-50 rounded-lg border border-amber-200"
|
|||
|
|
>
|
|||
|
|
<p className="text-sm text-amber-800 font-medium mb-1">
|
|||
|
|
{typeof warning === 'string' ? warning : warning.message}
|
|||
|
|
</p>
|
|||
|
|
{typeof warning === 'object' && warning.suggestion && (
|
|||
|
|
<p className="text-xs text-amber-700 italic">
|
|||
|
|
💡 {warning.suggestion}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|