summaryrefslogtreecommitdiff
path: root/components/client-table/import-utils.ts
blob: bc7f4b4430e20b2cf6c80dcf098df85fb4e778ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import ExcelJS from "exceljs"

interface ImportExcelOptions {
  file: File
  /**
   * Map Excel header names to data keys.
   * Example: { "Name": "name", "Age": "age" }
   */
  columnMapping?: Record<string, string>
  /**
   * Row offset to start reading data (0-based).
   * Default: 1 (assuming row 0 is header)
   */
  dataStartRow?: number
}

export interface ExcelImportResult<T = any> {
  data: T[]
  errors: string[]
}

/**
 * Generic function to read an Excel file and convert it to an array of objects.
 * Does NOT handle database insertion or validation logic.
 */
export async function importFromExcel<T = any>({
  file,
  columnMapping = {},
  dataStartRow = 1,
}: ImportExcelOptions): Promise<ExcelImportResult<T>> {
  const workbook = new ExcelJS.Workbook()
  const arrayBuffer = await file.arrayBuffer()
  
  try {
    await workbook.xlsx.load(arrayBuffer)
  } catch (error) {
     return { data: [], errors: ["Failed to parse Excel file. Please ensure it is a valid .xlsx file."] }
  }

  const worksheet = workbook.worksheets[0] // Read the first sheet
  const data: T[] = []
  const errors: string[] = []

  if (!worksheet) {
    return { data: [], errors: ["No worksheet found in the Excel file."] }
  }

  // 1. Read Header Row (assumed to be row 1 for mapping if no explicit mapping provided, 
  //    or we can use it to validate mapping)
  const headers: string[] = []
  const headerRow = worksheet.getRow(1)
  headerRow.eachCell((cell, colNumber) => {
    headers[colNumber] = String(cell.value).trim()
  })

  // 2. Iterate Data Rows
  worksheet.eachRow((row, rowNumber) => {
    if (rowNumber <= dataStartRow) return // Skip header/pre-header rows

    const rowData: any = {}
    let hasData = false

    row.eachCell((cell, colNumber) => {
      const headerText = headers[colNumber]
      if (!headerText) return

      // Determine the key to use for this column
      // Priority: Explicit mapping -> Header text as key
      const key = columnMapping[headerText] || headerText

      let cellValue = cell.value

      // Handle ExcelJS object values (e.g. formula results, hyperlinks)
      if (cellValue && typeof cellValue === 'object') {
        if ('result' in cellValue) {
             // Formula result
             cellValue = (cellValue as any).result
        } else if ('text' in cellValue) {
             // Hyperlink text
             cellValue = (cellValue as any).text
        } else if ('richText' in cellValue) {
             // Rich text
             cellValue = (cellValue as any).richText.map((t: any) => t.text).join('')
        }
      }
      
      if (cellValue !== null && cellValue !== undefined && String(cellValue).trim() !== '') {
          hasData = true
          rowData[key] = cellValue
      }
    })

    if (hasData) {
      data.push(rowData as T)
    }
  })

  return { data, errors }
}