581 lines
18 KiB
TypeScript
581 lines
18 KiB
TypeScript
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)
|
||
})
|
||
}
|
||
}
|