sozsoft-platform/ui/src/components/importManager/ImportDashboard.tsx

824 lines
39 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import React, { useState, useEffect, useMemo } from 'react'
2026-05-13 11:31:15 +00:00
import classNames from 'classnames'
2026-02-24 20:44:16 +00:00
import {
FaUpload,
FaCheckCircle,
FaRegBell,
FaClock,
FaSync,
FaEye,
FaTrashAlt,
FaDownload,
FaFileExcel,
FaFileAlt,
2026-05-13 19:07:50 +00:00
FaExclamationTriangle,
FaChevronDown,
FaChevronUp,
2026-02-24 20:44:16 +00:00
} from 'react-icons/fa'
import { FileUploadArea } from './FileUploadArea'
import { ImportPreview } from './ImportPreview'
import { ImportProgress } from './ImportProgress'
2026-05-22 09:06:15 +00:00
import { ListFormImportDto, ListFormImportLogDto } from '@/proxy/imports/models'
2026-02-24 20:44:16 +00:00
import { ImportService } from '@/services/import.service'
import { GridDto } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
2026-05-13 11:31:15 +00:00
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
2026-02-24 20:44:16 +00:00
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<
2026-05-22 09:06:15 +00:00
Record<string, ListFormImportLogDto[]>
2026-02-24 20:44:16 +00:00
>({})
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 {
2026-05-22 09:06:15 +00:00
const executes = await importService.getListFormImportLogs(sessionId)
2026-02-24 20:44:16 +00:00
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)
}
}
2026-05-22 09:06:15 +00:00
const handleImportLog = async (
2026-02-24 20:44:16 +00:00
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':
2026-05-13 19:07:50 +00:00
case 'executed':
2026-02-24 20:44:16 +00:00
return <FaCheckCircle className="w-5 h-5 text-green-500" />
2026-05-13 19:07:50 +00:00
case 'executed_with_errors':
return <FaExclamationTriangle className="w-5 h-5 text-orange-500" />
2026-02-24 20:44:16 +00:00
case 'failed':
2026-05-13 19:07:50 +00:00
case 'execute_failed':
2026-02-24 20:44:16 +00:00
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':
2026-05-13 19:07:50 +00:00
case 'executed':
2026-06-08 08:52:30 +00:00
return 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
2026-05-13 19:07:50 +00:00
case 'executed_with_errors':
2026-06-08 08:52:30 +00:00
return 'bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-900/60'
2026-02-24 20:44:16 +00:00
case 'failed':
2026-05-13 19:07:50 +00:00
case 'execute_failed':
2026-06-08 08:52:30 +00:00
return 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
2026-02-24 20:44:16 +00:00
case 'processing':
case 'validating':
2026-06-08 08:52:30 +00:00
return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900/60'
2026-02-24 20:44:16 +00:00
default:
2026-06-08 08:52:30 +00:00
return 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-300 dark:border-yellow-900/60'
2026-02-24 20:44:16 +00:00
}
}
2026-05-13 19:07:50 +00:00
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':
2026-06-08 08:52:30 +00:00
return 'text-green-600 dark:text-green-300'
2026-05-13 19:07:50 +00:00
case 'completed_with_errors':
2026-06-08 08:52:30 +00:00
return 'text-orange-600 dark:text-orange-300'
2026-05-13 19:07:50 +00:00
case 'processing':
2026-06-08 08:52:30 +00:00
return 'text-blue-600 dark:text-blue-300'
2026-05-13 19:07:50 +00:00
case 'validating':
2026-06-08 08:52:30 +00:00
return 'text-yellow-600 dark:text-yellow-300'
2026-05-13 19:07:50 +00:00
case 'failed':
2026-06-08 08:52:30 +00:00
return 'text-red-600 dark:text-red-300'
2026-05-13 19:07:50 +00:00
default:
2026-06-08 08:52:30 +00:00
return 'text-slate-600 dark:text-slate-400'
2026-05-13 19:07:50 +00:00
}
}
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 []
}
}
2026-02-24 20:44:16 +00:00
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 = () => {
2026-05-13 19:07:50 +00:00
return gridDto.columnFormats.filter((col: any) => col.canCreate && col.fieldName !== 'Id')
2026-02-24 20:44:16 +00:00
}
const editableColumns = getEditableColumns()
2026-05-13 11:31:15 +00:00
const { isMaximized } = useDialogContext()
2026-02-24 20:44:16 +00:00
return (
2026-05-13 11:31:15 +00:00
<div className="flex flex-col w-full mt-4">
2026-02-24 20:44:16 +00:00
{/* Navigation Tabs */}
2026-06-08 08:52:30 +00:00
<div className="flex space-x-1 mb-4 bg-white dark:bg-slate-900 rounded-lg p-1 shadow-sm border border-slate-200 dark:border-slate-700 flex-shrink-0">
2026-02-24 20:44:16 +00:00
{['import', 'preview', 'history'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as TabNames)}
2026-05-13 11:31:15 +00:00
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
2026-02-24 20:44:16 +00:00
activeTab === tab
? 'bg-blue-500 text-white shadow-md'
2026-06-08 08:52:30 +00:00
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50 dark:text-slate-300 dark:hover:text-slate-100 dark:hover:bg-slate-800'
2026-02-24 20:44:16 +00:00
}`}
>
{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 */}
2026-05-13 11:31:15 +00:00
<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">
2026-06-08 08:52:30 +00:00
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
<div className="px-3 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
2026-05-13 11:31:15 +00:00
<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}
2026-06-08 08:52:30 +00:00
className="flex items-center gap-1.5 px-3 py-1.5 border border-green-200 dark:border-green-900/60 rounded-md hover:border-green-300 hover:bg-green-50 dark:hover:bg-green-950/30 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-slate-900 text-xs"
2026-05-13 11:31:15 +00:00
>
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
2026-06-08 08:52:30 +00:00
<span className="font-medium text-slate-700 dark:text-slate-200">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
</span>
</button>
<button
onClick={() => generateTemplate('csv')}
disabled={generating}
2026-06-08 08:52:30 +00:00
className="flex items-center gap-1.5 px-3 py-1.5 border border-blue-200 dark:border-blue-900/60 rounded-md hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/30 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-slate-900 text-xs"
2026-05-13 11:31:15 +00:00
>
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
2026-06-08 08:52:30 +00:00
<span className="font-medium text-slate-700 dark:text-slate-200">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.CsvTemplate')}
</span>
</button>
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
<div className="max-h-96 overflow-y-auto">
<table className="w-full">
2026-06-08 08:52:30 +00:00
<thead className="bg-slate-100 dark:bg-slate-800 sticky top-0">
2026-05-13 11:31:15 +00:00
<tr>
2026-06-08 08:52:30 +00:00
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
2026-05-13 11:31:15 +00:00
{translate('::App.Listform.ListformField.Column')}
</th>
2026-06-08 08:52:30 +00:00
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
2026-05-13 11:31:15 +00:00
{translate('::ListForms.ListFormEdit.Type')}
</th>
2026-06-08 08:52:30 +00:00
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
2026-05-13 11:31:15 +00:00
{translate('::App.Required')}
</th>
2026-06-08 08:52:30 +00:00
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
2026-05-13 11:31:15 +00:00
{translate('::Abp.Mailing.Default')}
</th>
2026-02-24 20:44:16 +00:00
</tr>
2026-05-13 11:31:15 +00:00
</thead>
2026-06-08 08:52:30 +00:00
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
2026-05-13 11:31:15 +00:00
{editableColumns.map((column: any) => (
2026-06-08 08:52:30 +00:00
<tr key={column.fieldName} className="hover:bg-slate-50 dark:hover:bg-slate-800/70">
<td className="px-2 py-2 font-medium text-slate-800 dark:text-slate-100">
2026-05-13 19:07:50 +00:00
{column.fieldName}
2026-05-13 11:31:15 +00:00
</td>
2026-06-08 08:52:30 +00:00
<td className="px-4 py-2 text-slate-600 dark:text-slate-300">
2026-05-13 11:31:15 +00:00
<span
className={`px-2 py-1 rounded text-xs font-medium ${
column.dataType === 'string'
2026-06-08 08:52:30 +00:00
? 'bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300'
2026-05-13 11:31:15 +00:00
: column.dataType === 'number'
2026-06-08 08:52:30 +00:00
? 'bg-green-100 text-green-800 dark:bg-green-950/40 dark:text-green-300'
2026-05-13 11:31:15 +00:00
: column.dataType === 'boolean'
2026-06-08 08:52:30 +00:00
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300'
: 'bg-orange-100 text-orange-800 dark:bg-orange-950/40 dark:text-orange-300'
2026-05-13 11:31:15 +00:00
}`}
>
{column.dataType}
</span>
</td>
<td className="px-4 py-2">
{column.validationRuleDto.some(
(rule: any) => rule.type === 'required',
) ? (
2026-06-08 08:52:30 +00:00
<span className="text-red-500 dark:text-red-300 font-medium">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.Yes')}
</span>
) : (
2026-06-08 08:52:30 +00:00
<span className="text-slate-400 dark:text-slate-500">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.No')}
</span>
)}
</td>
2026-06-08 08:52:30 +00:00
<td className="px-4 py-2 text-slate-600 dark:text-slate-300 text-sm">
2026-05-13 11:31:15 +00:00
{typeof column.defaultValue === 'object'
? JSON.stringify(column.defaultValue)
: column.defaultValue}
</td>
</tr>
))}
</tbody>
</table>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
{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>
2026-06-08 08:52:30 +00:00
<span className="ml-2 text-slate-600 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.GeneratingTemplate')}
</span>
</div>
)}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
{/* File Upload - 1/3 width on large screens, full width on mobile */}
<div className="lg:col-span-1">
2026-06-08 08:52:30 +00:00
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-4 h-full">
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-100 mb-4 flex items-center">
2026-05-13 11:31:15 +00:00
<FaUpload className="w-5 h-5 mr-2 text-green-500" />
{translate('::App.Listforms.ImportManager.UploadData')}
</h2>
<FileUploadArea
onFileUpload={handleFileUpload}
2026-02-24 20:44:16 +00:00
loading={loading}
2026-05-13 11:31:15 +00:00
acceptedFormats={['.xlsx', '.xls', '.csv']}
2026-02-24 20:44:16 +00:00
/>
</div>
2026-05-13 11:31:15 +00:00
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
</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}
2026-05-22 09:06:15 +00:00
onExecute={handleImportLog}
2026-05-13 11:31:15 +00:00
loading={loading}
importService={importService}
onPreviewLoaded={loadImportHistory}
/>
</div>
)}
</div>
) : (
2026-06-08 08:52:30 +00:00
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-12">
<div className="text-center text-slate-500 dark:text-slate-400">
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300 dark:text-slate-600" />
2026-05-13 11:31:15 +00:00
<div className="text-xl font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoDataToPreview')}
</div>
<div>{translate('::App.Listforms.ImportManager.UploadFileToPreview')}</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 11:31:15 +00:00
)}
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
)}
{activeTab === 'history' && (
2026-06-08 08:52:30 +00:00
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
2026-05-13 11:31:15 +00:00
<FaClock className="w-5 h-5 mr-2 text-indigo-500" />
{translate('::App.Listforms.ImportManager.ImportHistory')}
</h2>
</div>
2026-02-24 20:44:16 +00:00
2026-06-08 08:52:30 +00:00
<div className="divide-y divide-slate-100 dark:divide-slate-800">
2026-05-13 11:31:15 +00:00
{importHistory.map((session) => (
<div
key={session.id}
className={`p-2 transition-colors border-l-4 ${
currentSession?.id === session.id
2026-06-08 08:52:30 +00:00
? 'bg-blue-50 border-l-blue-500 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/40'
: 'border-l-transparent hover:bg-slate-50 dark:hover:bg-slate-800/70'
2026-05-13 11:31:15 +00:00
}`}
>
<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>
2026-06-08 08:52:30 +00:00
<div className="font-medium text-slate-800 dark:text-slate-100">
{session.blobName}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{new Date(session.creationTime).toLocaleString()}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
{currentSession?.id === session.id && (
2026-06-08 08:52:30 +00:00
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300">
2026-05-13 11:31:15 +00:00
{translate('::App.Status.Active')}
</span>
)}
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 11:31:15 +00:00
<div className="flex items-center space-x-4">
<div className="text-right">
2026-06-08 08:52:30 +00:00
<div className="text-sm font-medium text-slate-800 dark:text-slate-100">
2026-05-13 11:31:15 +00:00
{session.totalRows} {translate('::App.Listforms.ImportManager.TotalRows')}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
<span
className={`px-3 py-1 rounded-full text-xs font-medium border ${getStatusColor(
session.status,
)}`}
2026-02-24 20:44:16 +00:00
>
2026-05-13 19:07:50 +00:00
{getSessionStatusLabel(session.status)}
2026-05-13 11:31:15 +00:00
</span>
<div className="flex space-x-2">
<button
onClick={() => toggleSessionExecutes(session.id)}
className={`p-2 rounded-lg transition-colors ${
expandedSessions.has(session.id)
2026-06-08 08:52:30 +00:00
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100 dark:bg-red-950/30 dark:text-red-300 dark:hover:bg-red-950/40'
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100 dark:text-slate-500 dark:hover:text-slate-300 dark:hover:bg-slate-800'
2026-05-13 11:31:15 +00:00
}`}
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 {
2026-05-22 09:06:15 +00:00
const executes = await importService.getListFormImportLogs(
2026-05-13 11:31:15 +00:00
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
})
}
2026-02-24 20:44:16 +00:00
}
2026-05-13 11:31:15 +00:00
}}
2026-06-08 08:52:30 +00:00
className="p-2 rounded-lg transition-colors text-slate-400 hover:text-blue-500 hover:bg-blue-50 dark:text-slate-500 dark:hover:text-blue-300 dark:hover:bg-blue-950/30"
2026-05-13 11:31:15 +00:00
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
2026-06-08 08:52:30 +00:00
? 'text-slate-300 dark:text-slate-600 cursor-not-allowed'
: 'text-slate-400 hover:text-red-500 hover:bg-red-50 dark:text-slate-500 dark:hover:text-red-300 dark:hover:bg-red-950/30'
2026-05-13 11:31:15 +00:00
}`}
title={
currentSession?.id === session.id
? translate('::App.Listforms.ImportManager.CannotDeleteActiveSession')
: translate('::App.Listforms.ImportManager.DeleteImportSession')
2026-02-24 20:44:16 +00:00
}
2026-05-13 11:31:15 +00:00
>
<FaTrashAlt className="w-4 h-4" />
</button>
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 19:07:50 +00:00
{/* 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'
2026-06-08 08:52:30 +00:00
? 'bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
2026-05-13 19:07:50 +00:00
: session.status === 'uploaded'
2026-06-08 08:52:30 +00:00
? 'bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900/60'
: 'bg-green-50 text-green-700 border border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
2026-05-13 19:07:50 +00:00
}`}
>
<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>
)}
2026-05-13 11:31:15 +00:00
{/* Execute Details Section */}
{expandedSessions.has(session.id) && (
2026-06-08 08:52:30 +00:00
<div className="mt-3 bg-gradient-to-r from-indigo-50 to-blue-50 dark:from-slate-800 dark:to-slate-800 border border-indigo-100 dark:border-slate-700 rounded-lg shadow-sm hover:shadow-md transition-shadow">
2026-05-13 11:31:15 +00:00
<div className="p-3">
{loadingExecutes.has(session.id) ? (
2026-06-08 08:52:30 +00:00
<div className="flex items-center space-x-2 text-slate-500 dark:text-slate-400 py-2">
2026-05-13 11:31:15 +00:00
<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) => (
2026-06-08 08:52:30 +00:00
<div key={execute.id} className="p-3 rounded-lg dark:bg-slate-900/40">
2026-05-13 11:31:15 +00:00
<div className="flex items-center justify-between">
{/* Sol: Tarih */}
<div className="flex-shrink-0">
2026-06-08 08:52:30 +00:00
<div className="text-lg text-slate-500 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{new Date(execute.creationTime).toLocaleString()}
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 11:31:15 +00:00
{/* Orta: Executed, Valid, Errors */}
2026-06-08 08:52:30 +00:00
<div className="flex items-center space-x-4 text-xs text-slate-600 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
<div className="text-center">
2026-06-08 08:52:30 +00:00
<div className="font-medium text-slate-800 dark:text-slate-100">
2026-05-13 11:31:15 +00:00
{execute.execRows}
</div>
2026-06-08 08:52:30 +00:00
<div className="text-slate-500 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.Executed')}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
<div className="text-center">
2026-06-08 08:52:30 +00:00
<div className="font-medium text-green-600 dark:text-green-300">
2026-05-13 11:31:15 +00:00
{execute.validRows}
</div>
2026-06-08 08:52:30 +00:00
<div className="text-slate-500 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.Valid')}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
<div className="text-center">
2026-06-08 08:52:30 +00:00
<div className="font-medium text-red-600 dark:text-red-300">
2026-05-13 11:31:15 +00:00
{execute.errorRows}
</div>
2026-06-08 08:52:30 +00:00
<div className="text-slate-500 dark:text-slate-400">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.Errors')}
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 11:31:15 +00:00
{/* Sağ: Status */}
<div className="flex items-center space-x-2 flex-shrink-0">
2026-05-13 19:07:50 +00:00
{getExecuteStatusIcon(execute.status)}
2026-05-13 11:31:15 +00:00
<div
2026-05-13 19:07:50 +00:00
className={`text-xs font-medium ${getExecuteStatusColor(execute.status)}`}
2026-05-13 11:31:15 +00:00
>
2026-05-13 19:07:50 +00:00
{getExecuteStatusLabel(execute.status)}
2026-05-13 11:31:15 +00:00
{execute.status === 'processing' && ` (${execute.progress}%)`}
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-05-13 19:07:50 +00:00
{/* Error Details */}
{(execute.status === 'completed_with_errors' ||
execute.status === 'failed') &&
execute.errorRows > 0 && (
<div className="mt-2">
<button
onClick={() => toggleErrors(execute.id)}
2026-06-08 08:52:30 +00:00
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"
2026-05-13 19:07:50 +00:00
>
{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) && (
2026-06-08 08:52:30 +00:00
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50 dark:border-orange-900/60 dark:bg-orange-950/30">
2026-05-13 19:07:50 +00:00
{parseErrors(execute.errorsJson).length > 0 ? (
<table className="w-full text-xs">
2026-06-08 08:52:30 +00:00
<thead className="bg-orange-100 dark:bg-orange-950/50 sticky top-0">
2026-05-13 19:07:50 +00:00
<tr>
2026-06-08 08:52:30 +00:00
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
2026-05-13 19:07:50 +00:00
Satır
</th>
2026-06-08 08:52:30 +00:00
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
2026-05-13 19:07:50 +00:00
Hata Mesajı
</th>
</tr>
</thead>
2026-06-08 08:52:30 +00:00
<tbody className="divide-y divide-orange-100 dark:divide-orange-900/50">
2026-05-13 19:07:50 +00:00
{parseErrors(execute.errorsJson).map((err, idx) => (
2026-06-08 08:52:30 +00:00
<tr
key={idx}
className="hover:bg-orange-100 dark:hover:bg-orange-950/50"
>
<td className="px-3 py-1 text-orange-700 dark:text-orange-300 font-medium">
2026-05-13 19:07:50 +00:00
{err.row}
</td>
2026-06-08 08:52:30 +00:00
<td className="px-3 py-1 text-slate-700 dark:text-slate-200">
2026-05-13 19:07:50 +00:00
{err.message}
</td>
</tr>
))}
</tbody>
</table>
) : (
2026-06-08 08:52:30 +00:00
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
2026-05-13 19:07:50 +00:00
Hata detayı mevcut değil.
</p>
)}
</div>
)}
</div>
)}
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
))}
</div>
) : (
2026-06-08 08:52:30 +00:00
<div className="text-sm text-slate-500 dark:text-slate-400 py-2">
2026-05-13 11:31:15 +00:00
{translate('::App.Listforms.ImportManager.NoExecutionRecords')}
</div>
)}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
)}
</div>
))}
2026-02-24 20:44:16 +00:00
2026-05-13 11:31:15 +00:00
{importHistory.length === 0 && (
2026-06-08 08:52:30 +00:00
<div className="p-12 text-center text-slate-500 dark:text-slate-400">
<FaClock className="w-12 h-12 mx-auto mb-4 text-slate-300 dark:text-slate-600" />
2026-05-13 11:31:15 +00:00
<div className="text-lg font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoImportHistory')}
</div>
<div>{translate('::App.Listforms.ImportManager.ImportHistoryHint')}</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
)}
</div>
2026-02-24 20:44:16 +00:00
</div>
2026-05-13 11:31:15 +00:00
)}
</div>
2026-02-24 20:44:16 +00:00
</div>
)
}