816 lines
39 KiB
TypeScript
816 lines
39 KiB
TypeScript
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>
|
||
)
|
||
}
|