2025-08-12 09:39:09 +00:00
|
|
|
|
import { GridDto } from '@/proxy/form/models'
|
2025-08-11 06:34:44 +00:00
|
|
|
|
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.visible && col.canCreate && !col.readOnly && 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]),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-08-12 09:39:09 +00:00
|
|
|
|
await this.updateSession(sessionId, {
|
|
|
|
|
|
totalRows: rows.length,
|
|
|
|
|
|
listFormCode: gridDto?.gridOptions.listFormCode || '',
|
|
|
|
|
|
})
|
2025-08-11 06:34:44 +00:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|