FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

183 linhas
5.6 KiB

  1. /**
  2. * Client-side .xlsx export with shared styling (headers, money format, alignment).
  3. * @see ./EXCEL_EXPORT_STANDARD.md — conventions for new Excel exports
  4. */
  5. import * as XLSX from "xlsx-js-style";
  6. /** Light grey header background + bold text (exported by xlsx-js-style). */
  7. const HEADER_CELL_STYLE: XLSX.CellStyle = {
  8. font: { bold: true, color: { rgb: "000000" } },
  9. fill: {
  10. patternType: "solid",
  11. fgColor: { rgb: "D9D9D9" },
  12. },
  13. alignment: { vertical: "center", horizontal: "center", wrapText: true },
  14. };
  15. function applyHeaderRowStyle(
  16. ws: XLSX.WorkSheet,
  17. columnCount: number
  18. ): void {
  19. for (let colIdx = 0; colIdx < columnCount; colIdx++) {
  20. const cellRef = XLSX.utils.encode_cell({ r: 0, c: colIdx });
  21. const cell = ws[cellRef];
  22. if (cell) {
  23. cell.s = { ...HEADER_CELL_STYLE };
  24. }
  25. }
  26. }
  27. /** Headers for money columns (GRN report & similar): thousands separator in Excel. */
  28. const MONEY_COLUMN_HEADER =
  29. /金額|單價|Amount|Unit Price|Total Amount|Total Amount \/ 金額|Amount \/ 金額|Unit Price \/ 單價/i;
  30. /** Quantity / numeric columns (right-align with amounts). */
  31. const QTY_COLUMN_HEADER = /Qty|數量|Demand/i;
  32. function isNumericDataColumnHeader(h: string): boolean {
  33. return MONEY_COLUMN_HEADER.test(h) || QTY_COLUMN_HEADER.test(h);
  34. }
  35. /**
  36. * Apply Excel number format `#,##0.00` to money columns so values show with comma separators.
  37. * Handles numeric cells and pre-formatted strings like "1,234.56".
  38. */
  39. function applyMoneyColumnNumberFormats(ws: XLSX.WorkSheet, headerLabels: string[]): void {
  40. const moneyColIdx = new Set<number>();
  41. headerLabels.forEach((h, c) => {
  42. if (MONEY_COLUMN_HEADER.test(h)) moneyColIdx.add(c);
  43. });
  44. if (moneyColIdx.size === 0 || !ws["!ref"]) return;
  45. const range = XLSX.utils.decode_range(ws["!ref"]);
  46. for (let r = range.s.r + 1; r <= range.e.r; r++) {
  47. moneyColIdx.forEach((c) => {
  48. const cellRef = XLSX.utils.encode_cell({ r, c });
  49. const cell = ws[cellRef];
  50. if (!cell) return;
  51. if (typeof cell.v === "number" && Number.isFinite(cell.v)) {
  52. cell.t = "n";
  53. cell.z = "#,##0.00";
  54. return;
  55. }
  56. if (typeof cell.v === "string") {
  57. const cleaned = cell.v.replace(/,/g, "").trim();
  58. if (cleaned === "") return;
  59. const n = Number.parseFloat(cleaned);
  60. if (!Number.isNaN(n)) {
  61. cell.v = n;
  62. cell.t = "n";
  63. cell.z = "#,##0.00";
  64. }
  65. }
  66. });
  67. }
  68. }
  69. /** Right-align amount / quantity column headers and data (merge with existing header fill). */
  70. function applyNumericColumnRightAlign(
  71. ws: XLSX.WorkSheet,
  72. headerLabels: string[]
  73. ): void {
  74. const numericColIdx = new Set<number>();
  75. headerLabels.forEach((h, c) => {
  76. if (isNumericDataColumnHeader(h)) numericColIdx.add(c);
  77. });
  78. if (numericColIdx.size === 0 || !ws["!ref"]) return;
  79. const range = XLSX.utils.decode_range(ws["!ref"]);
  80. for (let r = range.s.r; r <= range.e.r; r++) {
  81. numericColIdx.forEach((c) => {
  82. const cellRef = XLSX.utils.encode_cell({ r, c });
  83. const cell = ws[cellRef];
  84. if (!cell) return;
  85. const prev = (cell.s || {}) as XLSX.CellStyle;
  86. cell.s = {
  87. ...prev,
  88. font: prev.font,
  89. fill: prev.fill,
  90. border: prev.border,
  91. numFmt: prev.numFmt,
  92. alignment: {
  93. ...prev.alignment,
  94. horizontal: "right",
  95. vertical: "center",
  96. wrapText: prev.alignment?.wrapText ?? true,
  97. },
  98. };
  99. });
  100. }
  101. }
  102. /**
  103. * Export an array of row objects to a .xlsx file and trigger download.
  104. * @param rows Array of objects (keys become column headers)
  105. * @param filename Download filename (without .xlsx)
  106. * @param sheetName Optional sheet name (default "Sheet1")
  107. */
  108. export function exportChartToXlsx(
  109. rows: Record<string, unknown>[],
  110. filename: string,
  111. sheetName = "Sheet1"
  112. ): void {
  113. if (rows.length === 0) {
  114. const ws = XLSX.utils.aoa_to_sheet([[]]);
  115. const wb = XLSX.utils.book_new();
  116. XLSX.utils.book_append_sheet(wb, ws, sheetName);
  117. XLSX.writeFile(wb, `${filename}.xlsx`);
  118. return;
  119. }
  120. const ws = XLSX.utils.json_to_sheet(rows);
  121. // Auto-set column widths based on header length (simple heuristic).
  122. const header = Object.keys(rows[0] ?? {});
  123. if (header.length > 0) {
  124. ws["!cols"] = header.map((h) => ({
  125. // Basic width: header length + padding, minimum 12
  126. wch: Math.max(12, h.length + 4),
  127. }));
  128. applyHeaderRowStyle(ws, header.length);
  129. applyMoneyColumnNumberFormats(ws, header);
  130. applyNumericColumnRightAlign(ws, header);
  131. }
  132. const wb = XLSX.utils.book_new();
  133. XLSX.utils.book_append_sheet(wb, ws, sheetName);
  134. XLSX.writeFile(wb, `${filename}.xlsx`);
  135. }
  136. export type MultiSheetSpec = { name: string; rows: Record<string, unknown>[] };
  137. /**
  138. * Export multiple worksheets in one .xlsx file.
  139. * Sheet names are truncated to 31 characters (Excel limit).
  140. */
  141. export function exportMultiSheetToXlsx(
  142. sheets: MultiSheetSpec[],
  143. filename: string
  144. ): void {
  145. const wb = XLSX.utils.book_new();
  146. for (const { name, rows } of sheets) {
  147. const safeName = name.slice(0, 31);
  148. let ws: ReturnType<typeof XLSX.utils.json_to_sheet>;
  149. if (rows.length === 0) {
  150. ws = XLSX.utils.aoa_to_sheet([[]]);
  151. } else {
  152. ws = XLSX.utils.json_to_sheet(rows);
  153. const header = Object.keys(rows[0] ?? {});
  154. if (header.length > 0) {
  155. ws["!cols"] = header.map((h) => ({
  156. wch: Math.max(12, h.length + 4),
  157. }));
  158. applyHeaderRowStyle(ws, header.length);
  159. applyMoneyColumnNumberFormats(ws, header);
  160. applyNumericColumnRightAlign(ws, header);
  161. }
  162. }
  163. XLSX.utils.book_append_sheet(wb, ws, safeName);
  164. }
  165. XLSX.writeFile(wb, `${filename}.xlsx`);
  166. }