feat: improve machines inventory exports

This commit is contained in:
codex-bot 2025-10-30 16:09:06 -03:00
parent d92c817e7b
commit 38b46f32ce
5 changed files with 858 additions and 222 deletions

View file

@ -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")