sozsoft-platform/ui/src/components/importManager/ImportDashboard.tsx
2026-06-08 14:57:43 +03:00

816 lines
39 KiB
TypeScript
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, useMemo } from 'react'
import classNames from 'classnames'
import {
FaUpload,
FaCheckCircle,
FaRegBell,
FaClock,
FaSync,
FaEye,
FaTrashAlt,
FaDownload,
FaFileExcel,
FaFileAlt,
FaExclamationTriangle,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa'
import { FileUploadArea } from './FileUploadArea'
import { ImportPreview } from './ImportPreview'
import { ImportProgress } from './ImportProgress'
import { ListFormImportDto, ListFormImportLogDto } from '@/proxy/imports/models'
import { ImportService } from '@/services/import.service'
import { GridDto } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
interface ImportDashboardProps {
gridDto: GridDto
}
export type TabNames = 'import' | 'preview' | 'history'
export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) => {
const { translate } = useLocalization()
const [activeTab, setActiveTab] = useState<TabNames>('import')
const [currentSession, setCurrentSession] = useState<ListFormImportDto | null>(null)
const [importHistory, setImportHistory] = useState<ListFormImportDto[]>([])
const [loading, setLoading] = useState(false)
const importService = useMemo(() => new ImportService(), [])
const [generating, setGenerating] = useState(false)
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set())
const [sessionExecutes, setSessionExecutes] = useState<
Record<string, ListFormImportLogDto[]>
>({})
const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set())
useEffect(() => {
loadImportHistory()
}, [])
const loadImportHistory = async () => {
try {
const history = await importService.getListFormImportByListFormCode(
gridDto.gridOptions.listFormCode || '',
)
setImportHistory(history)
// Execute cache'ini temizle çünkü history değişmiş olabilir
setSessionExecutes({})
setExpandedSessions(new Set())
} catch (error) {
console.error('Failed to load import history:', error)
}
}
const toggleSessionExecutes = async (sessionId: string) => {
// Toggle expanded state
const newExpandedSessions = new Set(expandedSessions)
if (expandedSessions.has(sessionId)) {
newExpandedSessions.delete(sessionId)
setExpandedSessions(newExpandedSessions)
} else {
newExpandedSessions.add(sessionId)
setExpandedSessions(newExpandedSessions)
// Her zaman fresh data çek - cache'e güvenme
setLoadingExecutes((prev) => new Set([...prev, sessionId]))
try {
const executes = await importService.getListFormImportLogs(sessionId)
setSessionExecutes((prev) => ({
...prev,
[sessionId]: executes,
}))
} catch (error) {
console.error('Failed to load import executes:', error)
} finally {
setLoadingExecutes((prev) => {
const newSet = new Set(prev)
newSet.delete(sessionId)
return newSet
})
}
}
}
const handleFileUpload = async (file: File) => {
setLoading(true)
try {
let session = await importService.uploadFile(file, gridDto.gridOptions.listFormCode || '')
setCurrentSession(session)
setActiveTab('preview')
// Start polling for status updates - continue until fully processed
if (session.status !== 'failed') {
// Daha uzun süreli ve gerçekçi bir progression
setTimeout(async () => {
session = await importService.updateSession(session.id, {
status: 'validating',
listFormCode: gridDto.gridOptions.listFormCode || '',
})
setCurrentSession(session)
}, 1000) // 1 saniye sonra validating
setTimeout(async () => {
session = await importService.updateSession(session.id, {
status: 'processing',
listFormCode: gridDto.gridOptions.listFormCode || '',
})
setCurrentSession(session)
}, 2000) // 2 saniye sonra processing başlangıç
setTimeout(async () => {
session = await importService.updateSession(session.id, {
status: 'uploaded',
listFormCode: gridDto.gridOptions.listFormCode || '',
})
setCurrentSession(session)
}, 3000) // 3 saniye sonra uploaded
pollImportStatus(session.id)
}
// Load import history to refresh the data
await loadImportHistory()
} catch (error) {
console.error('File upload failed:', error)
} finally {
setLoading(false)
}
}
const pollImportStatus = async (sessionId: string) => {
try {
const session = await importService.getListFormImport(sessionId)
setCurrentSession(session)
if (session.status === 'uploaded' || session.status === 'failed') {
loadImportHistory()
}
} catch (error) {
console.error('Failed to get import status:', error)
}
}
const handleImportLog = async (
sessionId: string,
listFormCode: string,
selectedRows?: number[],
) => {
setLoading(true)
try {
const session = await importService.executeImport(sessionId, listFormCode, selectedRows)
pollImportStatus(session.importId)
await loadImportHistory()
// Execute sonrası History sekmesine geç ki kullanıcı yeni execute'i görebilsin
setActiveTab('history')
} catch (error) {
console.error('Import execution failed:', error)
} finally {
setLoading(false)
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'uploaded':
case 'executed':
return <FaCheckCircle className="w-5 h-5 text-green-500" />
case 'executed_with_errors':
return <FaExclamationTriangle className="w-5 h-5 text-orange-500" />
case 'failed':
case 'execute_failed':
return <FaRegBell className="w-5 h-5 text-red-500" />
case 'processing':
case 'validating':
return <FaSync className="w-5 h-5 text-blue-500 animate-spin" />
default:
return <FaClock className="w-5 h-5 text-yellow-500" />
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'uploaded':
case 'executed':
return 'bg-green-50 text-green-700 border-green-200 dark:bg-gray-800 dark:text-green-300 dark:border-gray-700'
case 'executed_with_errors':
return 'bg-orange-50 text-orange-700 border-orange-200 dark:bg-gray-800 dark:text-orange-300 dark:border-gray-700'
case 'failed':
case 'execute_failed':
return 'bg-red-50 text-red-700 border-red-200 dark:bg-gray-800 dark:text-red-300 dark:border-gray-700'
case 'processing':
case 'validating':
return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-gray-800 dark:text-blue-300 dark:border-gray-700'
default:
return 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-gray-800 dark:text-yellow-300 dark:border-gray-700'
}
}
const getSessionStatusLabel = (status: string) => {
switch (status) {
case 'uploading':
return translate('::App.Listforms.ImportManager.Uploading')
case 'validating':
return translate('::App.Listforms.ImportManager.Validating')
case 'processing':
return translate('::App.Listforms.ImportManager.Processing')
case 'uploaded':
return translate('::App.Listforms.ImportManager.Uploaded')
case 'failed':
return translate('::App.Listforms.ImportManager.Failed')
case 'executed':
return translate('::App.Listforms.ImportManager.Executed')
case 'executed_with_errors':
return translate('::App.Listforms.ImportManager.ExecutedWithErrors')
case 'execute_failed':
return translate('::App.Listforms.ImportManager.ExecuteFailed')
default:
return status.charAt(0).toUpperCase() + status.slice(1)
}
}
const getSessionGuidance = (status: string) => {
switch (status) {
case 'uploaded':
return translate('::App.Listforms.ImportManager.UploadedDescription')
case 'executed':
return translate('::App.Listforms.ImportManager.ExecutedDescription')
case 'executed_with_errors':
return translate('::App.Listforms.ImportManager.ExecutedWithErrorsDescription')
case 'execute_failed':
return translate('::App.Listforms.ImportManager.ExecuteFailedDescription')
case 'failed':
return translate('::App.Listforms.ImportManager.FailedDescription')
default:
return null
}
}
const getExecuteStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <FaCheckCircle className="w-4 h-4 text-green-500" />
case 'completed_with_errors':
return <FaExclamationTriangle className="w-4 h-4 text-orange-500" />
case 'processing':
return <FaSync className="w-4 h-4 text-blue-500 animate-spin" />
case 'validating':
return <FaClock className="w-4 h-4 text-yellow-500" />
case 'failed':
return <FaRegBell className="w-4 h-4 text-red-500" />
default:
return null
}
}
const getExecuteStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-green-600 dark:text-green-300'
case 'completed_with_errors':
return 'text-orange-600 dark:text-orange-300'
case 'processing':
return 'text-blue-600 dark:text-blue-300'
case 'validating':
return 'text-yellow-600 dark:text-yellow-300'
case 'failed':
return 'text-red-600 dark:text-red-300'
default:
return 'text-gray-600 dark:text-gray-400'
}
}
const getExecuteStatusLabel = (status: string) => {
if (status === 'completed_with_errors') return 'Completed with errors'
return status.charAt(0).toUpperCase() + status.slice(1)
}
const [expandedErrors, setExpandedErrors] = useState<Set<string>>(new Set())
const toggleErrors = (executeId: string) => {
setExpandedErrors((prev) => {
const next = new Set(prev)
if (next.has(executeId)) next.delete(executeId)
else next.add(executeId)
return next
})
}
const parseErrors = (errorsJson?: string): { row: number; message: string }[] => {
if (!errorsJson) return []
try {
return JSON.parse(errorsJson)
} catch {
return []
}
}
const generateTemplate = async (format: 'excel' | 'csv') => {
setGenerating(true)
try {
const blob = await importService.generateTemplate(gridDto, format)
const filename = `${gridDto.gridOptions.listFormCode}_template.${format === 'excel' ? 'xlsx' : 'csv'}`
importService.downloadGenerateTemplate(blob, filename)
} catch (error) {
console.error('Template generation failed:', error)
} finally {
setGenerating(false)
}
}
const getEditableColumns = () => {
return gridDto.columnFormats.filter((col: any) => col.canCreate && col.fieldName !== 'Id')
}
const editableColumns = getEditableColumns()
const { isMaximized } = useDialogContext()
return (
<div className="flex flex-col w-full mt-4">
{/* Navigation Tabs */}
<div className="flex space-x-1 mb-4 bg-white dark:bg-gray-800 dark:border-gray-800 rounded-lg p-1 shadow-sm border border-gray-200 flex-shrink-0">
{['import', 'preview', 'history'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as TabNames)}
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
activeTab === tab
? 'bg-blue-500 text-white shadow-md'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-gray-100 dark:hover:bg-gray-800'
}`}
>
{tab === 'import' && <FaUpload className="w-4 h-4" />}
{tab === 'preview' && <FaEye className="w-4 h-4" />}
{tab === 'history' && <FaClock className="w-4 h-4" />}
<span className="capitalize">{tab}</span>
</button>
))}
</div>
{/* Content */}
<div className={classNames(isMaximized ? 'flex-1 min-h-0 overflow-auto' : '')}>
{activeTab === 'import' && (
<div className="space-y-6">
{/* Template Generator & File Upload - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Template Generator - 2/3 width on large screens, full width on mobile */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="px-3 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 className="text-xl font-semibold text-gray-800 dark:text-gray-100 flex items-center">
<FaDownload className="w-4 h-4 mr-2" />
{translate('::App.Listforms.ImportManager.TemplateColumns')} (
{editableColumns.length})
</h3>
{/* Template Options */}
<div className="flex gap-2">
<button
onClick={() => generateTemplate('excel')}
disabled={generating}
className="flex items-center gap-1.5 px-3 py-1.5 border border-green-200 dark:border-gray-700 rounded-md hover:border-green-300 hover:bg-green-50 dark:hover:bg-gray-800 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-900 text-xs"
>
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
<span className="font-medium text-gray-700 dark:text-gray-200">
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
</span>
</button>
<button
onClick={() => generateTemplate('csv')}
disabled={generating}
className="flex items-center gap-1.5 px-3 py-1.5 border border-blue-200 dark:border-gray-700 rounded-md hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-gray-800 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-900 text-xs"
>
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
<span className="font-medium text-gray-700 dark:text-gray-200">
{translate('::App.Listforms.ImportManager.CsvTemplate')}
</span>
</button>
</div>
</div>
<div className="max-h-96 overflow-y-auto">
<table className="w-full">
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{translate('::App.Listform.ListformField.Column')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{translate('::ListForms.ListFormEdit.Type')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{translate('::App.Required')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{translate('::Abp.Mailing.Default')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{editableColumns.map((column: any) => (
<tr key={column.fieldName} className="hover:bg-gray-50 dark:hover:bg-gray-800/70">
<td className="px-2 py-2 font-medium text-gray-800 dark:text-gray-100">
{column.fieldName}
</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
column.dataType === 'string'
? 'bg-blue-100 text-blue-800 dark:bg-gray-800 dark:text-blue-300'
: column.dataType === 'number'
? 'bg-green-100 text-green-800 dark:bg-gray-800 dark:text-green-300'
: column.dataType === 'boolean'
? 'bg-purple-100 text-purple-800 dark:bg-gray-800 dark:text-purple-300'
: 'bg-orange-100 text-orange-800 dark:bg-gray-800 dark:text-orange-300'
}`}
>
{column.dataType}
</span>
</td>
<td className="px-4 py-2">
{column.validationRuleDto.some(
(rule: any) => rule.type === 'required',
) ? (
<span className="text-red-500 dark:text-red-300 font-medium">
{translate('::App.Listforms.ImportManager.Yes')}
</span>
) : (
<span className="text-gray-400 dark:text-gray-500">
{translate('::App.Listforms.ImportManager.No')}
</span>
)}
</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 text-sm">
{typeof column.defaultValue === 'object'
? JSON.stringify(column.defaultValue)
: column.defaultValue}
</td>
</tr>
))}
</tbody>
</table>
</div>
{generating && (
<div className="flex items-center justify-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
<span className="ml-2 text-gray-600 dark:text-gray-400">
{translate('::App.Listforms.ImportManager.GeneratingTemplate')}
</span>
</div>
)}
</div>
</div>
{/* File Upload - 1/3 width on large screens, full width on mobile */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4 h-full">
<h2 className="text-xl font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
<FaUpload className="w-5 h-5 mr-2 text-green-500" />
{translate('::App.Listforms.ImportManager.UploadData')}
</h2>
<FileUploadArea
onFileUpload={handleFileUpload}
loading={loading}
acceptedFormats={['.xlsx', '.xls', '.csv']}
/>
</div>
</div>
</div>
</div>
)}
{activeTab === 'preview' && (
<div className="w-full">
{currentSession ? (
<div className="w-full">
{currentSession.status === 'validating' ||
currentSession.status === 'uploading' ||
currentSession.status === 'processing' ? (
<ImportProgress session={currentSession} />
) : (
<div className="w-full">
<ImportPreview
session={currentSession}
gridDto={gridDto}
onExecute={handleImportLog}
loading={loading}
importService={importService}
onPreviewLoaded={loadImportHistory}
/>
</div>
)}
</div>
) : (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-12">
<div className="text-center text-gray-500 dark:text-gray-400">
<FaEye className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" />
<div className="text-xl font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoDataToPreview')}
</div>
<div>{translate('::App.Listforms.ImportManager.UploadFileToPreview')}</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'history' && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{importHistory.map((session) => (
<div
key={session.id}
className={`p-2 transition-colors border-l-4 ${
currentSession?.id === session.id
? 'bg-blue-50 border-l-blue-500 hover:bg-blue-100 dark:bg-gray-800 dark:hover:bg-gray-700'
: 'border-l-transparent hover:bg-gray-50 dark:hover:bg-gray-800/70'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{getStatusIcon(session.status)}
<div className="flex items-center space-x-2">
<div>
<div className="font-medium text-gray-800 dark:text-gray-100">
{session.blobName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{new Date(session.creationTime).toLocaleString()}
</div>
</div>
{currentSession?.id === session.id && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-gray-800 dark:text-blue-300">
{translate('::App.Status.Active')}
</span>
)}
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<div className="text-sm font-medium text-gray-800 dark:text-gray-100">
{session.totalRows} {translate('::App.Listforms.ImportManager.TotalRows')}
</div>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-medium border ${getStatusColor(
session.status,
)}`}
>
{getSessionStatusLabel(session.status)}
</span>
<div className="flex space-x-2">
<button
onClick={() => toggleSessionExecutes(session.id)}
className={`p-2 rounded-lg transition-colors ${
expandedSessions.has(session.id)
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100 dark:bg-gray-800 dark:text-red-300 dark:hover:bg-gray-700'
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-500 dark:hover:text-gray-300 dark:hover:bg-gray-800'
}`}
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={async () => {
// Execute bilgilerini manuel olarak yenile
if (sessionExecutes[session.id]) {
setLoadingExecutes((prev) => new Set([...prev, session.id]))
try {
const executes = await importService.getListFormImportLogs(
session.id,
)
setSessionExecutes((prev) => ({
...prev,
[session.id]: executes,
}))
} catch (error) {
console.error('Failed to refresh import executes:', error)
} finally {
setLoadingExecutes((prev) => {
const newSet = new Set(prev)
newSet.delete(session.id)
return newSet
})
}
}
}}
className="p-2 rounded-lg transition-colors text-gray-400 hover:text-blue-500 hover:bg-blue-50 dark:text-gray-500 dark:hover:text-blue-300 dark:hover:bg-gray-800"
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
>
<FaSync className="w-4 h-4" />
</button>
<button
onClick={async () => {
if (currentSession?.id === session.id) {
return // Don't delete if it's the current session
}
await importService.deleteHistory(session.id)
await loadImportHistory()
}}
disabled={currentSession?.id === session.id}
className={`p-2 rounded-lg transition-colors ${
currentSession?.id === session.id
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
: 'text-gray-400 hover:text-red-500 hover:bg-red-50 dark:text-gray-500 dark:hover:text-red-300 dark:hover:bg-gray-800'
}`}
title={
currentSession?.id === session.id
? translate('::App.Listforms.ImportManager.CannotDeleteActiveSession')
: translate('::App.Listforms.ImportManager.DeleteImportSession')
}
>
<FaTrashAlt className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Guidance message */}
{getSessionGuidance(session.status) && (
<div
className={`mt-2 mx-1 px-3 py-2 rounded-md text-xs flex items-start space-x-2 ${
session.status === 'executed_with_errors' ||
session.status === 'failed' ||
session.status === 'execute_failed'
? 'bg-red-50 text-red-700 border border-red-200 dark:bg-gray-800 dark:text-red-300 dark:border-gray-700'
: session.status === 'uploaded'
? 'bg-blue-50 text-blue-700 border border-blue-200 dark:bg-gray-800 dark:text-blue-300 dark:border-gray-700'
: 'bg-green-50 text-green-700 border border-green-200 dark:bg-gray-800 dark:text-green-300 dark:border-gray-700'
}`}
>
<span className="mt-0.5 flex-shrink-0">
{session.status === 'executed_with_errors' ||
session.status === 'failed' ||
session.status === 'execute_failed' ? (
<FaExclamationTriangle className="w-3 h-3" />
) : session.status === 'uploaded' ? (
<FaRegBell className="w-3 h-3" />
) : (
<FaCheckCircle className="w-3 h-3" />
)}
</span>
<span>{getSessionGuidance(session.status)}</span>
</div>
)}
{/* Execute Details Section */}
{expandedSessions.has(session.id) && (
<div className="mt-3 bg-gradient-to-r from-indigo-50 to-blue-50 dark:from-gray-800 dark:to-gray-800 border border-indigo-100 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="p-3">
{loadingExecutes.has(session.id) ? (
<div className="flex items-center space-x-2 text-gray-500 dark:text-gray-400 py-2">
<FaSync className="w-4 h-4 animate-spin" />
<span className="text-sm">
{translate('::App.Listforms.ImportManager.LoadingExecutionDetails')}
</span>
</div>
) : sessionExecutes[session.id] &&
sessionExecutes[session.id].length > 0 ? (
<div className="space-y-2">
{sessionExecutes[session.id].map((execute) => (
<div key={execute.id} className="p-3 rounded-lg dark:bg-gray-900/40">
<div className="flex items-center justify-between">
{/* Sol: Tarih */}
<div className="flex-shrink-0">
<div className="text-lg text-gray-500 dark:text-gray-400">
{new Date(execute.creationTime).toLocaleString()}
</div>
</div>
{/* Orta: Executed, Valid, Errors */}
<div className="flex items-center space-x-4 text-xs text-gray-600 dark:text-gray-400">
<div className="text-center">
<div className="font-medium text-gray-800 dark:text-gray-100">
{execute.execRows}
</div>
<div className="text-gray-500 dark:text-gray-400">
{translate('::App.Listforms.ImportManager.Executed')}
</div>
</div>
<div className="text-center">
<div className="font-medium text-green-600 dark:text-green-300">
{execute.validRows}
</div>
<div className="text-gray-500 dark:text-gray-400">
{translate('::App.Listforms.ImportManager.Valid')}
</div>
</div>
<div className="text-center">
<div className="font-medium text-red-600 dark:text-red-300">
{execute.errorRows}
</div>
<div className="text-gray-500 dark:text-gray-400">
{translate('::App.Listforms.ImportManager.Errors')}
</div>
</div>
</div>
{/* Sağ: Status */}
<div className="flex items-center space-x-2 flex-shrink-0">
{getExecuteStatusIcon(execute.status)}
<div
className={`text-xs font-medium ${getExecuteStatusColor(execute.status)}`}
>
{getExecuteStatusLabel(execute.status)}
{execute.status === 'processing' && ` (${execute.progress}%)`}
</div>
</div>
</div>
{/* Error Details */}
{(execute.status === 'completed_with_errors' ||
execute.status === 'failed') &&
execute.errorRows > 0 && (
<div className="mt-2">
<button
onClick={() => toggleErrors(execute.id)}
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 dark:text-orange-300 dark:hover:text-orange-200 font-medium"
>
{expandedErrors.has(execute.id) ? (
<FaChevronUp className="w-3 h-3" />
) : (
<FaChevronDown className="w-3 h-3" />
)}
<span>
{execute.errorRows} hata detayı
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
</span>
</button>
{expandedErrors.has(execute.id) && (
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50 dark:border-gray-700 dark:bg-gray-800">
{parseErrors(execute.errorsJson).length > 0 ? (
<table className="w-full text-xs">
<thead className="bg-orange-100 dark:bg-gray-800 sticky top-0">
<tr>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
Satır
</th>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
Hata Mesajı
</th>
</tr>
</thead>
<tbody className="divide-y divide-orange-100 dark:divide-gray-700">
{parseErrors(execute.errorsJson).map((err, idx) => (
<tr
key={idx}
className="hover:bg-orange-100 dark:hover:bg-gray-700"
>
<td className="px-3 py-1 text-orange-700 dark:text-orange-300 font-medium">
{err.row}
</td>
<td className="px-3 py-1 text-gray-700 dark:text-gray-200">
{err.message}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
Hata detayı mevcut değil.
</p>
)}
</div>
)}
</div>
)}
</div>
))}
</div>
) : (
<div className="text-sm text-gray-500 dark:text-gray-400 py-2">
{translate('::App.Listforms.ImportManager.NoExecutionRecords')}
</div>
)}
</div>
</div>
)}
</div>
))}
{importHistory.length === 0 && (
<div className="p-12 text-center text-gray-500 dark:text-gray-400">
<FaClock className="w-12 h-12 mx-auto mb-4 text-gray-300 dark:text-gray-600" />
<div className="text-lg font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoImportHistory')}
</div>
<div>{translate('::App.Listforms.ImportManager.ImportHistoryHint')}</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
)
}