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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|