feat: improve machines inventory exports
This commit is contained in:
parent
d92c817e7b
commit
38b46f32ce
5 changed files with 858 additions and 222 deletions
101
src/lib/xlsx.ts
101
src/lib/xlsx.ts
|
|
@ -6,6 +6,12 @@ export type WorksheetConfig = {
|
|||
name: string
|
||||
headers: string[]
|
||||
rows: WorksheetRow[]
|
||||
columnWidths?: Array<number | null | undefined>
|
||||
freezePane?: {
|
||||
rowSplit?: number
|
||||
columnSplit?: number
|
||||
}
|
||||
autoFilter?: boolean
|
||||
}
|
||||
|
||||
type ZipEntry = {
|
||||
|
|
@ -38,22 +44,23 @@ function columnRef(index: number): string {
|
|||
return col
|
||||
}
|
||||
|
||||
function formatCell(value: unknown, colIndex: number, rowIndex: number): string {
|
||||
const ref = `${columnRef(colIndex)}${rowIndex + 1}`
|
||||
function formatCell(value: unknown, colIndex: number, rowNumber: number, styleIndex?: number): string {
|
||||
const ref = `${columnRef(colIndex)}${rowNumber}`
|
||||
const styleAttr = styleIndex !== undefined ? ` s="${styleIndex}"` : ""
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return `<c r="${ref}"/>`
|
||||
return `<c r="${ref}"${styleAttr}/>`
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return `<c r="${ref}" t="inlineStr"><is><t>${escapeXml(value.toISOString())}</t></is></c>`
|
||||
return `<c r="${ref}"${styleAttr} t="inlineStr"><is><t xml:space="preserve">${escapeXml(value.toISOString())}</t></is></c>`
|
||||
}
|
||||
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return `<c r="${ref}"><v>${value}</v></c>`
|
||||
return `<c r="${ref}"${styleAttr}><v>${value}</v></c>`
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return `<c r="${ref}"><v>${value ? 1 : 0}</v></c>`
|
||||
return `<c r="${ref}"${styleAttr}><v>${value ? 1 : 0}</v></c>`
|
||||
}
|
||||
|
||||
let text: string
|
||||
|
|
@ -62,25 +69,75 @@ function formatCell(value: unknown, colIndex: number, rowIndex: number): string
|
|||
} else {
|
||||
text = JSON.stringify(value)
|
||||
}
|
||||
return `<c r="${ref}" t="inlineStr"><is><t>${escapeXml(text)}</t></is></c>`
|
||||
return `<c r="${ref}"${styleAttr} t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`
|
||||
}
|
||||
|
||||
function buildWorksheetXml(config: WorksheetConfig): string {
|
||||
type WorksheetStyles = {
|
||||
header: number
|
||||
body: number
|
||||
}
|
||||
|
||||
function buildWorksheetXml(config: WorksheetConfig, styles: WorksheetStyles): string {
|
||||
const totalRows = config.rows.length + 1
|
||||
const rows: string[] = []
|
||||
const headerRow = config.headers.map((header, idx) => formatCell(header, idx, 0)).join("")
|
||||
const headerRow = config.headers.map((header, idx) => formatCell(header, idx, 1, styles.header)).join("")
|
||||
rows.push(`<row r="1">${headerRow}</row>`)
|
||||
|
||||
config.rows.forEach((rowData, rowIdx) => {
|
||||
const cells = config.headers.map((_, colIdx) => formatCell(rowData[colIdx], colIdx, rowIdx + 1)).join("")
|
||||
rows.push(`<row r="${rowIdx + 2}">${cells}</row>`)
|
||||
const actualRow = rowIdx + 2
|
||||
const cells = config.headers
|
||||
.map((_, colIdx) => formatCell(rowData[colIdx], colIdx, actualRow, styles.body))
|
||||
.join("")
|
||||
rows.push(`<row r="${actualRow}">${cells}</row>`)
|
||||
})
|
||||
|
||||
const hasCustomWidths = Array.isArray(config.columnWidths) && config.columnWidths.some((width) => typeof width === "number")
|
||||
const colsXml = hasCustomWidths
|
||||
? `<cols>${config.headers
|
||||
.map((_, idx) => {
|
||||
const width = config.columnWidths?.[idx]
|
||||
if (typeof width !== "number" || Number.isNaN(width)) {
|
||||
return `<col min="${idx + 1}" max="${idx + 1}"/>`
|
||||
}
|
||||
return `<col min="${idx + 1}" max="${idx + 1}" width="${width}" customWidth="1"/>`
|
||||
})
|
||||
.join("")}</cols>`
|
||||
: ""
|
||||
|
||||
let sheetViews = ""
|
||||
const rowSplit = config.freezePane?.rowSplit ?? 0
|
||||
const columnSplit = config.freezePane?.columnSplit ?? 0
|
||||
if (rowSplit > 0 || columnSplit > 0) {
|
||||
const attributes: string[] = []
|
||||
if (columnSplit > 0) attributes.push(`xSplit="${columnSplit}"`)
|
||||
if (rowSplit > 0) attributes.push(`ySplit="${rowSplit}"`)
|
||||
const topLeftColumn = columnSplit > 0 ? columnRef(columnSplit) : "A"
|
||||
const topLeftRow = rowSplit > 0 ? rowSplit + 1 : 1
|
||||
const activePane =
|
||||
rowSplit > 0 && columnSplit > 0
|
||||
? "bottomRight"
|
||||
: rowSplit > 0
|
||||
? "bottomLeft"
|
||||
: "topRight"
|
||||
const pane = `<pane ${attributes.join(" ")} topLeftCell="${topLeftColumn}${topLeftRow}" activePane="${activePane}" state="frozen"/>`
|
||||
sheetViews = `<sheetViews><sheetView workbookViewId="0">${pane}</sheetView></sheetViews>`
|
||||
}
|
||||
|
||||
const autoFilter =
|
||||
config.autoFilter && config.headers.length > 0 && totalRows > 1
|
||||
? `<autoFilter ref="A1:${columnRef(config.headers.length - 1)}${totalRows}"/>`
|
||||
: ""
|
||||
|
||||
return [
|
||||
XML_DECLARATION,
|
||||
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">',
|
||||
sheetViews,
|
||||
colsXml,
|
||||
' <sheetFormatPr defaultRowHeight="15"/>',
|
||||
" <sheetData>",
|
||||
rows.map((row) => ` ${row}`).join("\n"),
|
||||
" </sheetData>",
|
||||
autoFilter,
|
||||
"</worksheet>",
|
||||
].join("\n")
|
||||
}
|
||||
|
|
@ -206,6 +263,10 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
|
|||
throw new Error("Workbook requires at least one sheet")
|
||||
}
|
||||
|
||||
const styles: WorksheetStyles = {
|
||||
body: 0,
|
||||
header: 1,
|
||||
}
|
||||
const now = new Date()
|
||||
const timestamp = now.toISOString()
|
||||
const workbookRels: string[] = []
|
||||
|
|
@ -214,7 +275,7 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
|
|||
const sheetRefs = sheets.map((sheet, index) => {
|
||||
const sheetId = index + 1
|
||||
const relId = `rId${sheetId}`
|
||||
const worksheetXml = buildWorksheetXml(sheet)
|
||||
const worksheetXml = buildWorksheetXml(sheet, styles)
|
||||
sheetEntries.push({
|
||||
path: `xl/worksheets/sheet${sheetId}.xml`,
|
||||
data: Buffer.from(worksheetXml, "utf8"),
|
||||
|
|
@ -248,11 +309,21 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
|
|||
const stylesXml = [
|
||||
XML_DECLARATION,
|
||||
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">',
|
||||
' <fonts count="1"><font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/></font></fonts>',
|
||||
' <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>',
|
||||
' <fonts count="2">',
|
||||
' <font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/></font>',
|
||||
' <font><b/><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/></font>',
|
||||
" </fonts>",
|
||||
' <fills count="3">',
|
||||
' <fill><patternFill patternType="none"/></fill>',
|
||||
' <fill><patternFill patternType="gray125"/></fill>',
|
||||
' <fill><patternFill patternType="solid"><fgColor rgb="FFE2E8F0"/><bgColor indexed="64"/></patternFill></fill>',
|
||||
" </fills>",
|
||||
' <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>',
|
||||
' <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>',
|
||||
' <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>',
|
||||
' <cellXfs count="2">',
|
||||
' <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>',
|
||||
' <xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"/>',
|
||||
" </cellXfs>",
|
||||
' <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>',
|
||||
"</styleSheet>",
|
||||
].join("\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue