erp-platform/ui/src/services/import.service.ts
2025-08-17 00:51:17 +03:00

581 lines
18 KiB
TypeScript
Raw Blame History

import { GridDto } from '@/proxy/form/models'
import {
ImportPreviewData,
ListFormImportDto,
ListFormImportExecuteDto,
} from '@/proxy/imports/models'
import apiService from './api.service'
export class ImportService {
private _uploadedFiles = new Map<string, File>()
async generateTemplate(gridDto: GridDto, format: 'excel' | 'csv'): Promise<Blob> {
const editableColumns = gridDto.columnFormats
.filter((col) => col.permissionDto.i && col.fieldName !== 'Id')
.sort((a, b) => a.listOrderNo - b.listOrderNo)
if (format === 'excel') {
// Create Excel-compatible content
const headers = editableColumns.map((col) => col.captionName || col.fieldName)
// Create a simple Excel XML format that works with Excel 2010+
const excelContent = `<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40">
<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
<Title>Import Template</Title>
</DocumentProperties>
<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
<WindowHeight>12000</WindowHeight>
<WindowWidth>15000</WindowWidth>
<WindowTopX>240</WindowTopX>
<WindowTopY>75</WindowTopY>
<ProtectStructure>False</ProtectStructure>
<ProtectWindows>False</ProtectWindows>
</ExcelWorkbook>
<Styles>
<Style ss:ID="Default" ss:Name="Normal">
<Alignment ss:Vertical="Bottom"/>
<Borders/>
<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
<Interior/>
<NumberFormat/>
<Protection/>
</Style>
<Style ss:ID="s62">
<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000" ss:Bold="1"/>
<Interior ss:Color="#D9E1F2" ss:Pattern="Solid"/>
</Style>
</Styles>
<Worksheet ss:Name="Import Template">
<Table ss:ExpandedColumnCount="${
headers.length
}" ss:ExpandedRowCount="1000" x:FullColumns="1" x:FullRows="1" ss:DefaultRowHeight="15">
<Row ss:AutoFitHeight="0" ss:Height="18">
${headers
.map((header) => ` <Cell ss:StyleID="s62"><Data ss:Type="String">${header}</Data></Cell>`)
.join('\n')}
</Row>
</Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<PageSetup>
<Header x:Margin="0.3"/>
<Footer x:Margin="0.3"/>
<PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>
</PageSetup>
<Selected/>
<Panes>
<Pane>
<Number>3</Number>
<ActiveRow>1</ActiveRow>
<ActiveCol>0</ActiveCol>
</Pane>
</Panes>
<ProtectObjects>False</ProtectObjects>
<ProtectScenarios>False</ProtectScenarios>
</WorksheetOptions>
</Worksheet>
</Workbook>`
return new Blob([excelContent], {
type: 'application/vnd.ms-excel',
})
} else {
// CSV format
const headers = editableColumns.map((col) => col.captionName || col.fieldName)
const content = headers.join(',') + '\n'
return new Blob([content], { type: 'text/csv' })
}
}
downloadGenerateTemplate(blob: Blob, filename: string): void {
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
// Set proper file extension for Excel files
if (filename.endsWith('.xlsx') && blob.type === 'application/vnd.ms-excel') {
a.download = filename.replace('.xlsx', '.xls')
}
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
}
async uploadFile(file: File, listFormCode: string): Promise<ListFormImportDto> {
const entity = await this.createSession(file, listFormCode)
if (!entity.id) {
throw new Error('Session ID not returned from server')
}
this._uploadedFiles.set(entity.id, file)
return entity
}
async createSession(file: File, listFormCode: string): Promise<ListFormImportDto> {
const formData = new FormData()
formData.append('listFormCode', listFormCode)
formData.append('file', file)
formData.append('FileName', file.name)
formData.append('ContentType', file.type)
formData.append('ContentLength', file.size.toString())
const response = await apiService.fetchData<ListFormImportDto>({
url: `/api/app/list-form-import`,
method: 'POST',
data: formData as any,
})
return response.data
}
async getImportPreview(sessionId: string, gridDto?: GridDto): Promise<ImportPreviewData> {
const file = this._uploadedFiles.get(sessionId)
let actualData: any[] = []
if (file) {
try {
actualData = await this.parseFileForPreview(file)
} catch (error) {
console.error('Error parsing file for preview:', error)
}
} else {
console.log('No file found for session:', sessionId)
}
if (actualData.length === 0) {
return {
sessionId,
headers: [],
rows: [],
totalRows: 0,
columnMappings: [],
}
}
const headers = Object.keys(actualData[0])
const rows = actualData.map((row) => headers.map((header) => row[header] || ''))
const columnMappings = headers.map((header) => {
const matchingColumn = gridDto?.columnFormats.find(
(col) => col.captionName === header || col.fieldName === header,
)
return {
sourceColumn: header,
targetField: matchingColumn?.fieldName || header.toLowerCase().replace(/\s+/g, ''),
isRequired:
matchingColumn?.validationRuleDto.some((rule) => rule.type === 'required') || false,
dataType: matchingColumn?.dataType || this.inferDataType(header, actualData[0][header]),
}
})
await this.updateSession(sessionId, {
totalRows: rows.length,
listFormCode: gridDto?.gridOptions.listFormCode || '',
})
return {
sessionId,
headers,
rows,
totalRows: rows.length,
columnMappings,
validationResults: [],
}
}
async executeImport(
sessionId: string,
listFormCode: string,
selectedRows?: number[],
): Promise<ListFormImportExecuteDto> {
// Get the uploaded file data
const uploadedFile = this._uploadedFiles.get(sessionId)
if (!uploadedFile) {
throw new Error(`No uploaded file found for session ${sessionId}`)
}
// Parse the file to get all data
const allFileData = await this.parseFileForPreview(uploadedFile)
// Get selected rows data based on selectedRows indices
let selectedRowsData: any[] = []
if (selectedRows && selectedRows.length > 0) {
selectedRowsData = selectedRows
.filter((index) => index >= 0 && index < allFileData.length)
.map((index) => allFileData[index])
} else {
selectedRowsData = allFileData // If no specific rows selected, use all data
}
// Call backend API to execute import with selected rows data
const response = await apiService.fetchData<ListFormImportExecuteDto>({
url: `/api/app/list-form-import/execute`,
method: 'POST',
data: {
sessionId: sessionId,
listFormCode: listFormCode,
selectedRowsData: selectedRowsData,
selectedRowIndices: selectedRows || [],
},
})
return response.data
}
async getListFormImport(sessionId: string): Promise<ListFormImportDto> {
const response = await apiService.fetchData<ListFormImportDto>({
url: `/api/app/list-form-import/${sessionId}`,
method: 'GET',
})
return response.data
}
async getListFormImportByListFormCode(listFormCode: string): Promise<ListFormImportDto[]> {
const response = await apiService.fetchData<ListFormImportDto[]>({
url: `/api/app/list-form-import/by-list-form-code`,
method: 'GET',
params: { listFormCode },
})
return response.data
}
async getListFormImportExecutes(sessionId: string): Promise<ListFormImportExecuteDto[]> {
const response = await apiService.fetchData<ListFormImportExecuteDto[]>({
url: `/api/app/list-form-import/executes/${sessionId}`,
method: 'GET',
})
return response.data
}
async deleteHistory(sessionId: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/list-form-import/${sessionId}`,
method: 'DELETE',
})
}
async updateSession(
sessionId: string,
payload: Partial<ListFormImportDto>,
): Promise<ListFormImportDto> {
const response = await apiService.fetchData<ListFormImportDto>({
url: `/api/app/list-form-import/${sessionId}`,
method: 'PUT',
data: payload, // body
})
return response.data
}
private async parseFileForPreview(file: File): Promise<any[]> {
const extension = file.name.toLowerCase().split('.').pop()
if (extension === 'csv') {
return await this.parseCsvForPreview(file)
} else if (extension === 'xls' || extension === 'xlsx') {
return await this.parseExcelForPreview(file)
}
return []
}
private async parseCsvForPreview(file: File): Promise<any[]> {
try {
// Try multiple encodings to handle Turkish characters properly
const content = await this.tryMultipleEncodings(file, [
'UTF-8',
'windows-1254',
'iso-8859-9',
'iso-8859-1',
])
const lines = content.split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) {
return []
}
const headers = this.parseCsvLine(lines[0]).map((header) =>
this.fixTurkishCharacters(header.trim()),
)
const data: any[] = []
// Parse all data rows (excluding header)
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue // Skip empty lines
const values = this.parseCsvLine(lines[i])
const row: any = {}
headers.forEach((header, index) => {
let value = values[index] ? values[index].trim() : ''
// Fix Turkish character encoding issues
value = this.fixTurkishCharacters(value)
row[header] = value
})
data.push(row)
}
return data
} catch (error) {
console.error('Error parsing CSV:', error)
return []
}
}
private async parseExcelForPreview(file: File): Promise<any[]> {
// Since we can't easily parse Excel files in the browser without a library,
// we'll try to read it as text and extract what we can
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const content = e.target?.result as string
// Try to extract text content from Excel file
// This is a basic approach - in production you'd use a proper Excel parsing library
const textContent = this.extractTextFromExcel(content)
if (textContent.length > 0) {
resolve(textContent)
}
} catch (error) {
console.error('Error parsing Excel:', error)
resolve([])
}
}
reader.readAsText(file, 'UTF-8')
})
}
private extractTextFromExcel(content: string): any[] {
try {
const data: any[] = []
// Parse Excel XML format to extract data
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(content, 'text/xml')
// Check for parsing errors
const parseError = xmlDoc.querySelector('parsererror')
if (parseError) {
return this.extractDataWithRegex(content)
}
// Find all rows in the Excel XML
const rows = xmlDoc.querySelectorAll('Row')
if (rows.length < 2) {
return this.extractDataWithRegex(content)
}
// Extract headers from first row
const headerRow = rows[0]
const headerCells = headerRow.querySelectorAll('Cell Data')
const headers = Array.from(headerCells).map((cell) => {
let headerText = cell.textContent?.trim() || ''
// Fix Turkish character encoding issues in headers
return this.fixTurkishCharacters(headerText)
})
if (headers.length === 0) {
return this.extractDataWithRegex(content)
}
// Extract data from remaining rows
for (let i = 1; i < rows.length; i++) {
const row = rows[i]
const cells = row.querySelectorAll('Cell Data')
const rowData: any = {}
cells.forEach((cell, index) => {
if (index < headers.length && headers[index]) {
let cellValue = cell.textContent?.trim() || ''
// Clean up email links - extract just the email address
if (cellValue.includes('@') && cellValue.includes('mailto:')) {
const emailMatch = cellValue.match(/mailto:([^"']+)/)
if (emailMatch) {
cellValue = emailMatch[1]
}
}
// Fix Turkish character encoding issues
cellValue = this.fixTurkishCharacters(cellValue)
rowData[headers[index]] = cellValue
}
})
if (Object.keys(rowData).length > 0) {
data.push(rowData)
}
}
return data
} catch (error) {
console.error('Error extracting Excel data:', error)
return this.extractDataWithRegex(content)
}
}
private extractDataWithRegex(content: string): any[] {
try {
const data: any[] = []
// Extract all Data elements from Excel XML
const dataPattern = /<Data\s+ss:Type="String">([^<]+)<\/Data>/g
const matches = content.match(dataPattern)
if (!matches || matches.length === 0) {
return []
}
// Extract the actual text content from each match
const extractedValues = matches
.map((match) => {
const valueMatch = match.match(/<Data\s+ss:Type="String">([^<]+)<\/Data>/)
let value = valueMatch ? valueMatch[1].trim() : ''
// Fix Turkish character encoding issues
value = this.fixTurkishCharacters(value)
return value
})
.filter((value) => value)
// Group values into rows - assuming first row is headers
if (extractedValues.length >= 4) {
// At least one header row
const headers = extractedValues.slice(0, 4) // First 4 values as headers
// Group remaining values into rows of 4
const rowSize = headers.length
for (let i = rowSize; i < extractedValues.length; i += rowSize) {
const rowValues = extractedValues.slice(i, i + rowSize)
if (rowValues.length === rowSize) {
const rowData: any = {}
headers.forEach((header, index) => {
rowData[header] = rowValues[index] || ''
})
data.push(rowData)
}
}
}
return data
} catch (error) {
console.error('Error in regex extraction:', error)
return []
}
}
private parseCsvLine(line: string): string[] {
const result: string[] = []
let current = ''
let inQuotes = false
// Detect delimiter by counting occurrences (prioritize semicolon if it exists)
const semicolonCount = (line.match(/;/g) || []).length
const delimiter = semicolonCount > 0 ? ';' : ','
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
if (i + 1 < line.length && line[i + 1] === '"') {
// Handle escaped quotes
current += '"'
i++ // Skip next quote
} else {
inQuotes = !inQuotes
}
} else if (char === delimiter && !inQuotes) {
result.push(current.trim())
current = ''
} else {
current += char
}
}
result.push(current.trim())
const cleanedResult = result.map((field) => field.replace(/^"(.*)"$/, '$1'))
return cleanedResult
}
private inferDataType(columnName: string, sampleValue: any): string {
// Check the column name for common patterns
const lowerName = columnName.toLowerCase()
if (lowerName.includes('email')) return 'string'
if (lowerName.includes('date') || lowerName.includes('time')) return 'date'
if (
lowerName.includes('active') ||
lowerName.includes('enabled') ||
lowerName.includes('disabled')
)
return 'boolean'
if (lowerName.includes('count') || lowerName.includes('number') || lowerName.includes('amount'))
return 'number'
// Check the sample value
if (sampleValue !== null && sampleValue !== undefined) {
const strValue = String(sampleValue).toLowerCase().trim()
// Boolean check
if (strValue === 'true' || strValue === 'false' || strValue === '1' || strValue === '0') {
return 'boolean'
}
// Number check
if (!isNaN(Number(strValue)) && strValue !== '') {
return 'number'
}
// Date check (basic patterns)
if (strValue.match(/^\d{4}-\d{2}-\d{2}/) || strValue.match(/^\d{2}\/\d{2}\/\d{4}/)) {
return 'date'
}
}
return 'string' // Default to string
}
private fixTurkishCharacters(text: string): string {
return text
}
private async tryMultipleEncodings(file: File, encodings: string[]): Promise<string> {
for (const encoding of encodings) {
try {
const content = await this.readFileWithEncoding(file, encoding)
// Check if the content looks good (no replacement characters)
if (!content.includes('<27>') && !content.includes('\ufffd')) {
return content
}
} catch (error) {
console.log(`Failed to read with encoding ${encoding}:`, error)
}
}
// Fallback to UTF-8 if all else fails
return this.readFileWithEncoding(file, 'UTF-8')
}
private readFileWithEncoding(file: File, encoding: string): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => resolve(e.target?.result as string)
reader.onerror = reject
reader.readAsText(file, encoding)
})
}
}