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

644 lines
29 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,
} 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>
)
}