Files
mkd/components/finance/ReportProcessing.tsx

283 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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>
);
};