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,
|
|
|
|
|
|
} from 'react-icons/fa'
|
|
|
|
|
|
import { FileUploadArea } from './FileUploadArea'
|
|
|
|
|
|
import { ImportPreview } from './ImportPreview'
|
|
|
|
|
|
import { ImportProgress } from './ImportProgress'
|
|
|
|
|
|
import { ListFormImportDto, ListFormImportExecuteDto } from '@/proxy/imports/models'
|
|
|
|
|
|
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<
|
|
|
|
|
|
Record<string, ListFormImportExecuteDto[]>
|
|
|
|
|
|
>({})
|
|
|
|
|
|
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.getListFormImportExecutes(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 handleImportExecute = 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':
|
|
|
|
|
|
return <FaCheckCircle className="w-5 h-5 text-green-500" />
|
|
|
|
|
|
case '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':
|
|
|
|
|
|
return 'bg-green-50 text-green-700 border-green-200'
|
|
|
|
|
|
case 'failed':
|
|
|
|
|
|
return 'bg-red-50 text-red-700 border-red-200'
|
|
|
|
|
|
case 'processing':
|
|
|
|
|
|
case 'validating':
|
|
|
|
|
|
return 'bg-blue-50 text-blue-700 border-blue-200'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'bg-yellow-50 text-yellow-700 border-yellow-200'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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.permissionDto.i && col.fieldName !== 'Id')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-05-13 11:31:15 +00:00
|
|
|
|
<div className="flex space-x-1 mb-4 bg-white rounded-lg p-1 shadow-sm border border-slate-200 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'
|
|
|
|
|
|
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
|
|
|
|
|
|
<div className="px-3 py-3 border-b flex items-center justify-between">
|
|
|
|
|
|
<h3 className="text-xl font-semibold text-slate-800 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 rounded-md hover:border-green-300 hover:bg-green-50 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
|
|
|
|
|
|
<span className="font-medium text-slate-700">
|
|
|
|
|
|
{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 rounded-md hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
|
|
|
|
|
|
<span className="font-medium text-slate-700">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<thead className="bg-slate-100 sticky top-0">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
|
|
|
|
|
{translate('::App.Listform.ListformField.Column')}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
|
|
|
|
|
{translate('::ListForms.ListFormEdit.Type')}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
|
|
|
|
|
{translate('::App.Required')}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
|
|
|
|
|
{translate('::Abp.Mailing.Default')}
|
|
|
|
|
|
</th>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</tr>
|
2026-05-13 11:31:15 +00:00
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody className="divide-y divide-slate-100">
|
|
|
|
|
|
{editableColumns.map((column: any) => (
|
|
|
|
|
|
<tr key={column.fieldName} className="hover:bg-slate-50">
|
|
|
|
|
|
<td className="px-2 py-2 font-medium text-slate-800">
|
|
|
|
|
|
{translate('::' + column.captionName)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="px-4 py-2 text-slate-600">
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
|
|
|
|
column.dataType === 'string'
|
|
|
|
|
|
? 'bg-blue-100 text-blue-800'
|
|
|
|
|
|
: column.dataType === 'number'
|
|
|
|
|
|
? 'bg-green-100 text-green-800'
|
|
|
|
|
|
: column.dataType === 'boolean'
|
|
|
|
|
|
? 'bg-purple-100 text-purple-800'
|
|
|
|
|
|
: 'bg-orange-100 text-orange-800'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{column.dataType}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="px-4 py-2">
|
|
|
|
|
|
{column.validationRuleDto.some(
|
|
|
|
|
|
(rule: any) => rule.type === 'required',
|
|
|
|
|
|
) ? (
|
|
|
|
|
|
<span className="text-red-500 font-medium">
|
|
|
|
|
|
{translate('::App.Listforms.ImportManager.Yes')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-slate-400">
|
|
|
|
|
|
{translate('::App.Listforms.ImportManager.No')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="px-4 py-2 text-slate-600 text-sm">
|
|
|
|
|
|
{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>
|
|
|
|
|
|
<span className="ml-2 text-slate-600">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 h-full">
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-slate-800 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}
|
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}
|
|
|
|
|
|
onExecute={handleImportExecute}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
importService={importService}
|
|
|
|
|
|
onPreviewLoaded={loadImportHistory}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-12">
|
|
|
|
|
|
<div className="text-center text-slate-500">
|
|
|
|
|
|
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300" />
|
|
|
|
|
|
<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' && (
|
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
|
|
|
|
|
|
<div className="p-3 border-b border-slate-200">
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-slate-800 flex items-center">
|
|
|
|
|
|
<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-05-13 11:31:15 +00:00
|
|
|
|
<div className="divide-y divide-slate-100">
|
|
|
|
|
|
{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'
|
|
|
|
|
|
: 'border-l-transparent hover:bg-slate-50'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<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-slate-800">{session.blobName}</div>
|
|
|
|
|
|
<div className="text-sm text-slate-500">
|
|
|
|
|
|
{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 && (
|
|
|
|
|
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<div className="text-sm font-medium text-slate-800">
|
|
|
|
|
|
{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 11:31:15 +00:00
|
|
|
|
{session.status.charAt(0).toUpperCase() + session.status.slice(1)}
|
|
|
|
|
|
</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'
|
|
|
|
|
|
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
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.getListFormImportExecutes(
|
|
|
|
|
|
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
|
|
|
|
}}
|
|
|
|
|
|
className="p-2 rounded-lg transition-colors text-slate-400 hover:text-blue-500 hover:bg-blue-50"
|
|
|
|
|
|
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-slate-300 cursor-not-allowed'
|
|
|
|
|
|
: 'text-slate-400 hover:text-red-500 hover:bg-red-50'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
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 11:31:15 +00:00
|
|
|
|
{/* Execute Details Section */}
|
|
|
|
|
|
{expandedSessions.has(session.id) && (
|
|
|
|
|
|
<div className="mt-3 bg-gradient-to-r from-indigo-50 to-blue-50 border border-indigo-100 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-slate-500 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">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
{/* Sol: Tarih */}
|
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
|
<div className="text-lg text-slate-500">
|
|
|
|
|
|
{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 */}
|
|
|
|
|
|
<div className="flex items-center space-x-4 text-xs text-slate-600">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="font-medium text-slate-800">
|
|
|
|
|
|
{execute.execRows}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-slate-500">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<div className="font-medium text-green-600">
|
|
|
|
|
|
{execute.validRows}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-slate-500">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
<div className="font-medium text-red-600">
|
|
|
|
|
|
{execute.errorRows}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-slate-500">
|
|
|
|
|
|
{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">
|
|
|
|
|
|
{execute.status === 'completed' && (
|
|
|
|
|
|
<FaCheckCircle className="w-4 h-4 text-green-500" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{execute.status === 'processing' && (
|
|
|
|
|
|
<FaSync className="w-4 h-4 text-blue-500 animate-spin" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{execute.status === 'validating' && (
|
|
|
|
|
|
<FaClock className="w-4 h-4 text-yellow-500" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{execute.status === 'failed' && (
|
|
|
|
|
|
<FaRegBell className="w-4 h-4 text-red-500" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`text-xs font-medium ${
|
|
|
|
|
|
execute.status === 'completed'
|
|
|
|
|
|
? 'text-green-600'
|
|
|
|
|
|
: execute.status === 'processing'
|
|
|
|
|
|
? 'text-blue-600'
|
|
|
|
|
|
: execute.status === 'validating'
|
|
|
|
|
|
? 'text-yellow-600'
|
|
|
|
|
|
: 'text-red-600'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{execute.status.charAt(0).toUpperCase() +
|
|
|
|
|
|
execute.status.slice(1)}
|
|
|
|
|
|
{execute.status === 'processing' && ` (${execute.progress}%)`}
|
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-13 11:31:15 +00:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="text-sm text-slate-500 py-2">
|
|
|
|
|
|
{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 && (
|
|
|
|
|
|
<div className="p-12 text-center text-slate-500">
|
|
|
|
|
|
<FaClock className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|