Files
mkd/components/finance/ReportProcessing.tsx
2026-02-04 00:17:04 +05:00

283 lines
11 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};