sozsoft-platform/ui/src/components/importManager/ImportPreview.tsx
2026-06-08 11:20:56 +03:00

344 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react'
import { FaCheckCircle, FaExclamationTriangle, FaEye, FaPlay, FaTimes } from 'react-icons/fa'
import { ListFormImportDto } from '@/proxy/imports/models'
import { GridDto } from '@/proxy/form/models'
import { ImportService } from '@/services/import.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface ImportPreviewProps {
session: ListFormImportDto
gridDto: GridDto
onExecute: (
sessionId: string,
listFormCode: string,
selectedRows?: number[],
) => Promise<{ successCount?: number } | void>
loading: boolean
importService: ImportService
onPreviewLoaded?: () => void
}
export const ImportPreview: React.FC<ImportPreviewProps> = ({
session,
gridDto,
onExecute,
loading,
importService,
onPreviewLoaded,
}) => {
const [previewData, setPreviewData] = useState<any>(null)
const [selectedRows, setSelectedRows] = useState<number[]>([])
const [selectAll, setSelectAll] = useState(false)
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
const [lastExecutionResult, setLastExecutionResult] = useState<{
selectedCount: number
successCount: number
} | null>(null)
const hasCalledOnPreviewLoaded = useRef(false)
const { translate } = useLocalization()
// Selection handlers
const handleRowSelect = (rowIndex: number) => {
setSelectedRows((prev) => {
const newSelection = prev.includes(rowIndex)
? prev.filter((index) => index !== rowIndex)
: [...prev, rowIndex]
return newSelection
})
}
const handleSelectAll = () => {
if (selectAll) {
// Deselect all
setSelectedRows([])
} else {
// Select all available rows
const allRowIndices = previewData?.rows
? Array.from({ length: previewData.rows.length }, (_, i) => i)
: []
setSelectedRows(allRowIndices)
}
}
// Update selectAll state when individual rows are selected
useEffect(() => {
if (previewData?.rows && previewData.rows.length > 0) {
const allSelected = selectedRows.length === previewData.rows.length
setSelectAll(allSelected)
} else {
setSelectAll(false)
}
}, [selectedRows, previewData?.rows])
useEffect(() => {
const loadData = async () => {
try {
const data = await importService.getImportPreview(session.id, gridDto)
setPreviewData(data)
// Reset selections when new data is loaded
setSelectedRows([])
setSelectAll(false)
// Reset success message when new data is loaded
setShowSuccessMessage(false)
setLastExecutionResult(null)
// Call onPreviewLoaded callback to refresh history (only once per session)
if (onPreviewLoaded && !hasCalledOnPreviewLoaded.current) {
hasCalledOnPreviewLoaded.current = true
onPreviewLoaded()
}
} catch (error) {
console.error('Failed to load preview data:', error)
}
}
// Reset the flag when session changes
hasCalledOnPreviewLoaded.current = false
loadData()
}, [session.id, importService, gridDto]) // Remove session.status, onSessionUpdate and onPreviewLoaded from dependencies
const getStatusColor = (status: string) => {
switch (status) {
case 'uploaded':
return 'text-green-600 bg-green-50 border-green-200 dark:text-green-300 dark:bg-green-950/30 dark:border-green-900/60'
case 'failed':
return 'text-red-600 bg-red-50 border-red-200 dark:text-red-300 dark:bg-red-950/30 dark:border-red-900/60'
case 'validating':
return 'text-yellow-600 bg-yellow-50 border-yellow-200 dark:text-yellow-300 dark:bg-yellow-950/30 dark:border-yellow-900/60'
default:
return 'text-blue-600 bg-blue-50 border-blue-200 dark:text-blue-300 dark:bg-blue-950/30 dark:border-blue-900/60'
}
}
const canExecute = (previewData?.rows?.length || 0) > 0 && selectedRows.length > 0 && !loading
// Execute handler to clear selections after execution
const handleExecute = async () => {
const selectedCount = selectedRows.length
try {
const result = await onExecute(session.id, session.listFormCode, selectedRows)
// Check if execution was successful
if (result && typeof result === 'object' && 'successCount' in result) {
const successCount = result.successCount || 0
setLastExecutionResult({ selectedCount, successCount })
// Show success message if all selected rows were processed successfully
if (successCount === selectedCount) {
setShowSuccessMessage(true)
// Hide success message after 5 seconds
setTimeout(() => {
setShowSuccessMessage(false)
}, 5000)
}
}
// Clear selections after execute
setSelectedRows([])
setSelectAll(false)
} catch (error) {
console.error('Import execution failed:', error)
// Clear selections even on error
setSelectedRows([])
setSelectAll(false)
}
}
return (
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
{/* Header */}
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-center gap-4">
{/* Başlık kısmı - Üstte mobile, solda desktop */}
<div className="flex items-center order-1 lg:order-none">
<FaEye className="w-5 h-5 mr-2 text-blue-500" />
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100">
{translate('::App.Listforms.ImportManager.ImportPreviewTitle')}
</h3>
</div>
{/* İstatistik kartları - Mobile'da alt alta, desktop'ta yan yana */}
<div className="order-3 lg:order-none lg:absolute lg:left-1/2 lg:transform lg:-translate-x-1/2">
<div className="flex flex-col sm:flex-row justify-center gap-2">
<div className="text-center px-3 py-1 bg-blue-50 dark:bg-blue-950/30 rounded-full border border-blue-200 dark:border-blue-900/60 font-bold text-blue-600 dark:text-blue-300">
{previewData?.rows?.length || session.totalRows || 0}{' '}
<span className="text-xs text-blue-700 dark:text-blue-300">
{translate('::App.Listforms.ImportManager.TotalRows')}
</span>
</div>
</div>
</div>
{/* Durum göstergesi - Altta mobile, sağda desktop */}
<div className="flex justify-center lg:justify-end items-center order-2 lg:order-none lg:ml-auto w-full lg:w-auto">
<span
className={`px-3 py-1 rounded-full text-sm font-medium border w-full lg:w-auto text-center ${getStatusColor(
session.status,
)}`}
>
{session.status.charAt(0).toUpperCase() + session.status.slice(1)}
</span>
</div>
</div>
</div>
{/* Preview Data */}
{previewData && previewData.headers && previewData.headers.length > 0 ? (
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-4">
{translate('::App.Listforms.ImportManager.DataPreviewTitle')}
</h4>
<div className="overflow-auto border border-slate-200 dark:border-slate-700 rounded-lg max-h-90">
<table className="w-full text-sm min-w-full">
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0 z-10">
<tr>
<th className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap w-12">
<input
type="checkbox"
checked={selectAll}
onChange={handleSelectAll}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</th>
{previewData.headers.map((header: string, index: number) => (
<th
key={index}
className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap"
>
{header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
{previewData.rows.map((row: any[], rowIndex: number) => (
<tr
key={rowIndex}
className={`hover:bg-slate-50 dark:hover:bg-slate-800/70 ${
selectedRows.includes(rowIndex) ? 'bg-blue-50 dark:bg-blue-950/30' : ''
}`}
>
<td className="px-4 py-2">
<input
type="checkbox"
checked={selectedRows.includes(rowIndex)}
onChange={() => handleRowSelect(rowIndex)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</td>
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
className="px-4 py-2 text-slate-600 dark:text-slate-300 whitespace-nowrap max-w-xs truncate"
>
{cell?.toString() || '-'}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
) : previewData && previewData.headers && previewData.headers.length === 0 ? (
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="text-center py-8">
<FaExclamationTriangle className="w-12 h-12 mx-auto mb-4 text-yellow-500" />
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
{translate('::App.Listforms.ImportManager.NoDataFoundTitle')}
</h4>
<p className="text-slate-600 dark:text-slate-400">
{translate('::App.Listforms.ImportManager.NoDataFoundDescription')}
</p>
</div>
</div>
) : (
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
{translate('::App.Listforms.ImportManager.LoadingPreviewTitle')}
</h4>
<p className="text-slate-600 dark:text-slate-400">
{translate('::App.Listforms.ImportManager.LoadingPreviewDescription')}
</p>
</div>
</div>
)}
{/* Actions */}
<div className="p-3">
{/* Success Message */}
{showSuccessMessage && lastExecutionResult && (
<div className="mb-4 p-4 bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900/60 rounded-lg">
<div className="flex items-center space-x-2 text-green-700 dark:text-green-300">
<FaCheckCircle className="w-5 h-5" />
<span className="font-medium">
{translate('::App.Listforms.ImportManager.ImportProgress.Status.Uploaded')}{' '}
{lastExecutionResult.successCount} of {lastExecutionResult.selectedCount} rows were
imported.
</span>
</div>
</div>
)}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{selectedRows.length === 0 && (previewData?.rows?.length || 0) > 0 && (
<div className="flex items-center space-x-2 text-orange-600 dark:text-orange-300">
<FaExclamationTriangle className="w-5 h-5" />
<span className="font-medium">
{translate('::App.Listforms.ImportManager.SelectRowsWarning')}
</span>
</div>
)}
{selectedRows.length > 0 && (
<div className="flex items-center space-x-2 text-blue-600 dark:text-blue-300">
<FaCheckCircle className="w-5 h-5" />
<span className="font-medium">
{selectedRows.length} {translate('::App.Listforms.ImportManager.RowsSelected')}
</span>
</div>
)}
{(previewData?.rows?.length || 0) === 0 && (
<div className="flex items-center space-x-2 text-red-600 dark:text-red-300">
<FaExclamationTriangle className="w-5 h-5" />
<span className="font-medium">
{translate('::App.Listforms.ImportManager.NoRowsAvailable')}
</span>
</div>
)}
</div>
<div className="flex space-x-3">
<button className="px-4 py-2 text-slate-600 hover:text-slate-800 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors flex items-center space-x-2">
<FaTimes className="w-4 h-4" />
<span>{translate('::Cancel')}</span>
</button>
<button
onClick={handleExecute}
disabled={!canExecute || loading}
className="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center space-x-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>{translate('::App.Listforms.Status.Processing')}</span>
</>
) : (
<>
<FaPlay className="w-4 h-4" />
<span>{translate('::App.Listforms.ImportManager.Button.ExecuteImport')}</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
)
}