ImportManager problemi giderildi

This commit is contained in:
Sedat Öztürk 2026-05-13 22:07:50 +03:00
parent 66037bf001
commit 3f44b23f57
11 changed files with 347 additions and 66 deletions

View file

@ -159,9 +159,11 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
try
{
// Process the selected rows data
var processedCount = request.SelectedRowsData?.Count ?? 0;
var selectedRowsData = request.SelectedRowsData ?? new List<Dictionary<string, object>>();
var processedCount = selectedRowsData.Count;
var validCount = 0;
var errorCount = 0;
var errorDetails = new List<object>();
if (processedCount > 0)
{
@ -173,9 +175,9 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
var lastUpdateIndex = 0;
// Process each row individually
for (int i = 0; i < request.SelectedRowsData.Count; i++)
for (int i = 0; i < selectedRowsData.Count; i++)
{
var rowData = request.SelectedRowsData[i];
var rowData = selectedRowsData[i];
try
{
@ -189,7 +191,7 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
{
// If database insert fails, count as error
errorCount++;
// Log the error if needed
errorDetails.Add(new { row = i + 1, message = ex.Message });
Logger.LogWarning("Error inserting row {RowIndex} during import for session {SessionId}: {ErrorMessage}", i, session.Id, ex.Message);
}
@ -209,23 +211,32 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
}
// Update session with final results
execute.Status = "completed";
execute.Status = errorCount > 0 ? "completed_with_errors" : "completed";
execute.Progress = 100;
execute.ExecRows = processedCount;
execute.ValidRows = validCount;
execute.ErrorRows = errorCount;
execute.ErrorsJson = errorDetails.Count > 0 ? JsonSerializer.Serialize(errorDetails) : null;
await _importSessionExecuteRepository.UpdateAsync(execute, autoSave: true);
// Update parent session status to reflect execute result
session.Status = errorCount > 0 ? "executed_with_errors" : "executed";
await _importSessionRepository.UpdateAsync(session, autoSave: true);
return ObjectMapper.Map<ListFormImportExecute, ListFormImportExecuteDto>(execute);
}
catch (Exception ex)
{
// Update session status to failed
// Update execute status to failed
execute.Status = "failed";
execute.Progress = 0;
await _importSessionExecuteRepository.UpdateAsync(execute, autoSave: true);
// Update parent session status to reflect failure
session.Status = "execute_failed";
await _importSessionRepository.UpdateAsync(session, autoSave: true);
throw new UserFriendlyException($"Import failed: {ex.Message}");
}
}

View file

@ -1476,6 +1476,84 @@
"en": "Charts",
"tr": "Grafikler"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Uploading",
"en": "Uploading...",
"tr": "Yükleniyor..."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Validating",
"en": "Validating...",
"tr": "Doğrulanıyor..."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Processing",
"en": "Processing...",
"tr": "İşleniyor..."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Uploaded",
"en": "Uploaded",
"tr": "Yüklendi"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.UploadedDescription",
"en": "File uploaded but not imported yet. You can review the rows from preview tab and import.",
"tr": "Dosya yüklendi fakat henüz içe aktarılmadı. Preview sekmesinden satırları inceleyip aktarabilirsiniz."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Failed",
"en": "Failed",
"tr": "Başarısız"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.FailedDescription",
"en": "An error occurred while uploading the file. Please make sure the file is a valid Excel/CSV file and try again.",
"tr": "Dosya yüklenirken hata oluştu. Dosyanın geçerli bir Excel/CSV dosyası olduğundan emin olun ve tekrar deneyin."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.Executed",
"en": "Executed",
"tr": "Başarılı"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.ExecutedDescription",
"en": "All rows have been imported successfully.",
"tr": "Tüm satırlar başarıyla içe aktarıldı."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.ExecutedWithErrors",
"en": "Executed with errors",
"tr": "Hatalarla Tamamlandı"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.ExecutedWithErrorsDescription",
"en": "Some rows have been imported successfully but some rows failed to import. You can review the error details below and fix the related rows to import again.",
"tr": "Bazı satırlar içe aktarılamadı. Hata detaylarını aşağıda inceleyip ilgili satırları düzelterek tekrar yükleyebilirsiniz."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.ExecuteFailed",
"en": "Execute Failed",
"tr": "İçe Aktarma Başarısız"
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.ExecuteFailedDescription",
"en": "Import failed due to an unexpected error. Please try again or contact your system administrator.",
"tr": "İçe aktarma işlemi beklenmedik bir hata nedeniyle başarısız oldu. Lütfen tekrar deneyin veya sistem yöneticisine başvurun."
},
{
"resourceName": "Platform",
"key": "App.Listforms.ImportManager.DropHere",

View file

@ -167,7 +167,7 @@ public static class ListFormSeeder_DefaultJsons
R = permissionName,
U = permissionName + ".Update",
E = true,
I = false,
I = true,
Deny = false
});
}

View file

@ -168,7 +168,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
CaptionName = "App.Listform.ListformField.Id",
CaptionName = "App.Listform.ListformField.Id",
Width = 0,
ListOrderNo = 1,
Visible = false,

View file

@ -11,6 +11,9 @@ import {
FaDownload,
FaFileExcel,
FaFileAlt,
FaExclamationTriangle,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa'
import { FileUploadArea } from './FileUploadArea'
import { ImportPreview } from './ImportPreview'
@ -173,8 +176,12 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
const getStatusIcon = (status: string) => {
switch (status) {
case 'uploaded':
case 'executed':
return <FaCheckCircle className="w-5 h-5 text-green-500" />
case 'executed_with_errors':
return <FaExclamationTriangle className="w-5 h-5 text-orange-500" />
case 'failed':
case 'execute_failed':
return <FaRegBell className="w-5 h-5 text-red-500" />
case 'processing':
case 'validating':
@ -187,8 +194,12 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
const getStatusColor = (status: string) => {
switch (status) {
case 'uploaded':
case 'executed':
return 'bg-green-50 text-green-700 border-green-200'
case 'executed_with_errors':
return 'bg-orange-50 text-orange-700 border-orange-200'
case 'failed':
case 'execute_failed':
return 'bg-red-50 text-red-700 border-red-200'
case 'processing':
case 'validating':
@ -198,6 +209,105 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
}
}
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':
return 'text-green-600'
case 'completed_with_errors':
return 'text-orange-600'
case 'processing':
return 'text-blue-600'
case 'validating':
return 'text-yellow-600'
case 'failed':
return 'text-red-600'
default:
return 'text-slate-600'
}
}
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 []
}
}
const generateTemplate = async (format: 'excel' | 'csv') => {
setGenerating(true)
try {
@ -212,7 +322,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
}
const getEditableColumns = () => {
return gridDto.columnFormats.filter((col: any) => col.permissionDto.i && col.fieldName !== 'Id')
return gridDto.columnFormats.filter((col: any) => col.canCreate && col.fieldName !== 'Id')
}
const editableColumns = getEditableColumns()
@ -304,7 +414,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{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)}
{column.fieldName}
</td>
<td className="px-4 py-2 text-slate-600">
<span
@ -458,7 +568,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
session.status,
)}`}
>
{session.status.charAt(0).toUpperCase() + session.status.slice(1)}
{getSessionStatusLabel(session.status)}
</span>
<div className="flex space-x-2">
@ -528,6 +638,34 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</div>
</div>
{/* 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'
? 'bg-red-50 text-red-700 border border-red-200'
: session.status === 'uploaded'
? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'bg-green-50 text-green-700 border border-green-200'
}`}
>
<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>
)}
{/* 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">
@ -582,35 +720,72 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{/* 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" />
)}
{getExecuteStatusIcon(execute.status)}
<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'
}`}
className={`text-xs font-medium ${getExecuteStatusColor(execute.status)}`}
>
{execute.status.charAt(0).toUpperCase() +
execute.status.slice(1)}
{getExecuteStatusLabel(execute.status)}
{execute.status === 'processing' && ` (${execute.progress}%)`}
</div>
</div>
</div>
{/* Error Details */}
{(execute.status === 'completed_with_errors' ||
execute.status === 'failed') &&
execute.errorRows > 0 && (
<div className="mt-2">
<button
onClick={() => toggleErrors(execute.id)}
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 font-medium"
>
{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) && (
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50">
{parseErrors(execute.errorsJson).length > 0 ? (
<table className="w-full text-xs">
<thead className="bg-orange-100 sticky top-0">
<tr>
<th className="px-3 py-1 text-left font-medium text-orange-700 w-16">
Satır
</th>
<th className="px-3 py-1 text-left font-medium text-orange-700">
Hata Mesajı
</th>
</tr>
</thead>
<tbody className="divide-y divide-orange-100">
{parseErrors(execute.errorsJson).map((err, idx) => (
<tr key={idx} className="hover:bg-orange-100">
<td className="px-3 py-1 text-orange-700 font-medium">
{err.row}
</td>
<td className="px-3 py-1 text-slate-700">
{err.message}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="px-3 py-2 text-orange-600">
Hata detayı mevcut değil.
</p>
)}
</div>
)}
</div>
)}
</div>
))}
</div>

View file

@ -4,7 +4,7 @@ export interface ListFormImportDto {
id: string
listFormCode: string
blobName: string
status: 'uploading' | 'validating' | 'processing' | 'uploaded' | 'failed'
status: 'uploading' | 'validating' | 'processing' | 'uploaded' | 'failed' | 'executed' | 'executed_with_errors' | 'execute_failed'
totalRows: number
creationTime: string
}
@ -13,11 +13,12 @@ export interface ListFormImportExecuteDto {
id: string
importId: string
blobName: string
status: 'processing' | 'validating' | 'completed' | 'failed'
status: 'processing' | 'validating' | 'completed' | 'completed_with_errors' | 'failed'
execRows: number
validRows: number
errorRows: number
progress: number
errorsJson?: string
creationTime: string
}

View file

@ -16,7 +16,7 @@ export class ImportService {
if (format === 'excel') {
// Create Excel-compatible content
const headers = editableColumns.map((col) => col.captionName || col.fieldName)
const headers = editableColumns.map((col) => col.fieldName)
// Create a simple Excel XML format that works with Excel 2010+
const excelContent = `<?xml version="1.0"?>
@ -85,7 +85,7 @@ ${headers
})
} else {
// CSV format
const headers = editableColumns.map((col) => col.captionName || col.fieldName)
const headers = editableColumns.map((col) => col.fieldName)
const content = headers.join(',') + '\n'
return new Blob([content], { type: 'text/csv' })
}

View file

@ -147,10 +147,11 @@ const ChartDrawer = ({
{translate('::App.Platform.ChartDrawer.ChartSeries')}
</h2>
<Button
size="xs"
icon={<FaTimes />}
type="button"
onClick={onClose}
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
className="border-0"
></Button>
</div>
@ -158,14 +159,14 @@ const ChartDrawer = ({
{/* Add Series Button */}
<div className="p-3 border-b">
<Button
size="sm"
block
variant="solid"
type="button"
size="sm"
icon={<FaPlus />}
onClick={() => {
setFieldValue('series', [...values.series, newSeriesValue()])
}}
className="w-full"
>
<div className="flex items-center justify-center gap-2">
{translate('::App.Platform.ChartDrawer.AddNewSeries')}
@ -190,10 +191,10 @@ const ChartDrawer = ({
</span>
<Button
type="button"
size="sm"
size="xs"
variant="plain"
icon={<FaMinus />}
className="text-red-500 hover:bg-red-100"
className="border-0 text-red-500 hover:bg-red-100"
onClick={() => {
remove(index)
}}

View file

@ -65,6 +65,7 @@ import { useListFormColumns } from './useListFormColumns'
import { DataType } from 'devextreme/common'
import { useStoreState } from '@/store/store'
import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
interface TreeProps {
listFormCode: string
@ -1241,6 +1242,17 @@ const Tree = (props: TreeProps) => {
<SubForms gridDto={gridDto!} formData={formData} level={level ?? 0} />
</>
)}
<Dialog
width={smaller.md ? '100%' : 1000}
isOpen={filterData.isImportModalOpen || false}
onClose={() => filterData.setIsImportModalOpen(false)}
onRequestClose={() => filterData.setIsImportModalOpen(false)}
>
<Dialog.Body className="flex flex-col">
<ImportDashboard gridDto={gridDto} />
</Dialog.Body>
</Dialog>
</div>
)}

View file

@ -21,8 +21,6 @@ import { usePWA } from '@/utils/hooks/usePWA'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { GanttRef } from 'devextreme-react/cjs/gantt'
import { useStoreState } from '@/store'
import { cssClass } from 'yet-another-react-lightbox/*'
import classNames from 'classnames'
export interface ISelectBoxData {
value?: string
@ -284,30 +282,34 @@ const useFilters = ({
icon: 'revert',
})
}
}
if (checkPermission(gridDto?.gridOptions.permissionDto.i)) {
menus.push({
text: translate('::ListForms.ListForm.ImportManager'),
id: 'importManager',
icon: 'upload',
})
}
// Import, Manage ve Export: filtre/state ayarından bağımsız olarak her zaman kontrol et
if (checkPermission(gridDto?.gridOptions.permissionDto.i)) {
menus.push({
text: translate('::ListForms.ListForm.ImportManager'),
id: 'importManager',
icon: 'upload',
})
}
if (checkPermission('App.Listforms.Listform.Update')) {
menus.push({
text: translate('::ListForms.ListForm.Manage'),
id: 'openManage',
icon: 'preferences',
})
}
if (checkPermission('App.Listforms.Listform.Update')) {
menus.push({
text: translate('::ListForms.ListForm.Manage'),
id: 'openManage',
icon: 'preferences',
})
}
if (checkPermission(gridDto?.gridOptions.permissionDto.e)) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'exportButton',
})
}
if (checkPermission(gridDto?.gridOptions.permissionDto.e)) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'exportButton',
})
}
if (menus.length > 0) {
// filtre islemleri ile ilgili fonksiyonlar sayisi cok oldugu icin menu icerisine alindi
const menuFilterButtons: ToolbarItem = {

View file

@ -250,6 +250,7 @@ const DatabaseSetup = () => {
<div className="flex gap-3">
{(status === 'idle' || status === 'error') && !dbExists && (
<Button
variant='solid'
onClick={startMigration}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"
>