@efa-one/sdk 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ sub-paths — this keeps `express` out of the frontend and `react` out of the ba
14
14
  |---|---|---|
15
15
  | `@efa-one/sdk/backend` | Node/Express | Auth + token exchange (`requireAuth`, `createExchangeRouter`, `requireInternalOrAuth` …), health router, service discovery + gateway client (`serviceClient`, `resolveService`), clients for audit/reporting/mail/notifications, permission resolution/registration (`getUserPermissions`, `registerPermissions`, `checkPermission`), capability registry (`registerApiMetadata`), backend i18n |
16
16
  | `@efa-one/sdk/frontend` | Browser/React | postMessage IPC (`registerAppInfo`, `sendAtStart`, `navigateToApp`, `notifyRouteChange` …), react-i18next factory (`initI18n`), `DevHeader` |
17
- | `@efa-one/sdk/frontend/ui` | Browser/React | The efa-one design-system kit: `Button`, `Input`, `Badge`, `Alert`, `Dialog`, `DropdownMenu`, `Tooltip`, `EmptyState`, `Skeleton`, `RecordDialog`, and the **`DataTable`** (sortable/filterable/groupable list with draggable, resizable columns and a per-user persisted view; reflows to stacked cards below 640px). Also exports `useIsMobile` — the platform-wide 640px breakpoint hook. Ship its companion CSS once: `import '@efa-one/sdk/frontend/ui/styles.css'` |
17
+ | `@efa-one/sdk/frontend/ui` | Browser/React | The efa-one design-system kit: `Button`, `Input`, `Badge`, `Alert`, `Dialog`, `DropdownMenu`, `Tooltip`, `EmptyState`, `Skeleton`, `RecordDialog`, and the **`DataTable`** (sortable/filterable/groupable list with draggable, resizable columns, a per-user persisted view and CSV/Excel export of the current view; reflows to stacked cards below 640px). Also exports `useIsMobile` — the platform-wide 640px breakpoint hook. Ship its companion CSS once: `import '@efa-one/sdk/frontend/ui/styles.css'` |
18
18
  | `@efa-one/sdk/frontend/format` | Browser | Platform-wide date/time display — `formatDate` (`01.09.2026`), `formatDateTime` (`01.09.2026, 11:05`), `formatTime`, `formatFileStamp`, `localeForLanguage`. Always use these instead of `toLocaleDateString`/`toLocaleString`: without an options object those render `1.9.2026`, and without a locale argument they follow the browser language (`9/1/2026`) |
19
19
  | `@efa-one/sdk/frontend/viewPreferences` | Browser/React | Persistence seam for `DataTable` — `createViewPreferencesClient()` (standard `/api/view-preferences/:listId` adapter) + `useViewPreferences()` |
20
20
 
@@ -223,6 +223,22 @@ before, it was missing until the user reset the view. New prop `groupsDefaultExp
223
223
  groups start expanded; groups the user collapses stay collapsed. No backend or kernel
224
224
  change.
225
225
 
226
+ **1.22.0 — export the current view.** The DataTable's gear menu now has „Excel" and
227
+ „CSV" buttons. Both export exactly what the user sees: filtered and sorted rows (in
228
+ group order when grouped, collapsed groups included), visible columns in their order.
229
+ CSV uses `;` as separator, `,` as decimal mark and a UTF-8 BOM, so German Excel opens
230
+ it by double-click; text that starts like a formula is prefixed with `'`. The .xlsx is
231
+ written without a dependency (stored ZIP): bold, light-grey header row with thin
232
+ borders, frozen, with autofilter; numbers and dates are real Excel values
233
+ (`dd.mm.yyyy`). Values come from `accessor`, not from `cell`; multi-select filter
234
+ codes are exported as their labels. New `ColumnDef` fields `exportValue` (own export
235
+ value) and `exportable: false` (leave out, e.g. action columns), new props
236
+ `exportFileName` (default: `listId`, plus a timestamp) and `exportable={false}` (hide
237
+ the export, e.g. without an export permission). `toCsv`/`toXlsx`/`downloadFile` are
238
+ exported for app-side use. CSV is CSV: Excel turns `01067` into `1067` and long digit
239
+ strings into `1,23E+18`, and shows the `'` in front of `+49 …` — point users to the
240
+ Excel export for such data. No backend or kernel change.
241
+
226
242
  ## License
227
243
 
228
244
  [Apache-2.0](./LICENSE) — permissive with a patent grant, so customers and partners
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { type ViewPreferencesAdapter } from '../viewPreferences.js';
3
+ import { type ExportCell, type ExportTable } from './tableExport.js';
3
4
  export type FilterDef = {
4
5
  type: 'text';
5
6
  } | {
@@ -23,6 +24,8 @@ export interface ColumnDef<T> {
23
24
  sortable?: boolean;
24
25
  width?: string;
25
26
  defaultVisible?: boolean;
27
+ exportValue?: (row: T) => string | number | boolean | Date | null | undefined;
28
+ exportable?: boolean;
26
29
  }
27
30
  interface FilterValue {
28
31
  text?: string;
@@ -58,11 +61,16 @@ interface DataTableProps<T, K extends string | number> {
58
61
  footer?: React.ReactNode;
59
62
  groupsDefaultExpanded?: boolean;
60
63
  persistence?: ViewPreferencesAdapter;
64
+ exportable?: boolean;
65
+ exportFileName?: string;
61
66
  }
62
67
  export declare function defaultPrefsFor<T>(columns: ColumnDef<T>[], overrides?: Partial<ViewPrefs>): ViewPrefs;
63
68
  export declare function reconcileOrder<T>(columns: ColumnDef<T>[], order: string[]): string[];
64
69
  export declare function isColumnVisible<T>(column: ColumnDef<T>, visibility: Record<string, boolean>): boolean;
65
70
  export declare function formatCellValue<T>(col: ColumnDef<T>, row: T): string;
71
+ export declare function exportCellOf<T>(col: ColumnDef<T>, row: T): ExportCell;
72
+ export declare function buildExportTable<T>(rows: T[], visibleColumns: ColumnDef<T>[]): ExportTable;
73
+ export declare function exportFileBase(name: string, now?: Date): string;
66
74
  export declare const MIN_COLUMN_WIDTH = 60;
67
75
  export declare const MAX_COLUMN_WIDTH = 1600;
68
76
  export declare function clampColumnWidth(px: number): number;
@@ -78,7 +86,7 @@ interface RenderItem<T> {
78
86
  expanded?: boolean;
79
87
  row?: T;
80
88
  }
81
- export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string>): RenderItem<T>[];
89
+ export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string> | 'all'): RenderItem<T>[];
82
90
  export declare function collectAllGroupKeys<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>): string[];
83
- export declare function DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, }: DataTableProps<T, K>): React.ReactElement;
91
+ export declare function DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, exportable, exportFileName, }: DataTableProps<T, K>): React.ReactElement;
84
92
  export {};
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import * as DropdownMenu from './DropdownMenu.js';
4
- import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X, GripVertical } from 'lucide-react';
4
+ import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X, GripVertical, Download } from 'lucide-react';
5
5
  import { Button } from './Button.js';
6
6
  import { useViewPreferences } from '../viewPreferences.js';
7
7
  import { useIsMobile } from '../useIsMobile.js';
8
- import { formatDate, formatDateTime } from '../format.js';
8
+ import { formatDate, formatDateTime, formatFileStamp } from '../format.js';
9
+ import { downloadFile, toCsv, toXlsx, trimChar, XLSX_MIME } from './tableExport.js';
9
10
  const GROUP_KEY_SEP = '\x1f';
10
11
  const EMPTY_GROUP_LABEL = '– ohne –';
11
12
  export const DEFAULT_VIEW_VERSION = 2;
@@ -54,6 +55,47 @@ export function formatCellValue(col, row) {
54
55
  }
55
56
  return String(v ?? '');
56
57
  }
58
+ export function exportCellOf(col, row) {
59
+ const v = col.exportValue ? col.exportValue(row) : col.accessor(row);
60
+ if (v == null || v === '')
61
+ return null;
62
+ if (isDateColumn(col, v)) {
63
+ const t = timeOf(v);
64
+ if (t != null)
65
+ return { date: new Date(t), withTime: col.type === 'datetime' };
66
+ return col.exportValue ? String(v) : null;
67
+ }
68
+ const f = col.exportValue ? undefined : col.filter;
69
+ if (typeof v === 'boolean') {
70
+ if (f?.type === 'boolean')
71
+ return v ? (f.labelTrue ?? 'Ja') : (f.labelFalse ?? 'Nein');
72
+ return v ? 'Ja' : 'Nein';
73
+ }
74
+ if (typeof v === 'number') {
75
+ if (!Number.isFinite(v))
76
+ return null;
77
+ const opt = f?.type === 'multi-select' ? f.options.find((o) => o.value === String(v)) : undefined;
78
+ return opt ? opt.label : v;
79
+ }
80
+ const labelOf = (x) => {
81
+ const s = String(x);
82
+ return f?.type === 'multi-select' ? (f.options.find((o) => o.value === s)?.label ?? s) : s;
83
+ };
84
+ if (Array.isArray(v))
85
+ return v.map(labelOf).join(', ');
86
+ return labelOf(v);
87
+ }
88
+ export function buildExportTable(rows, visibleColumns) {
89
+ const cols = visibleColumns.filter((c) => c.exportable !== false);
90
+ return {
91
+ headers: cols.map((c) => c.label),
92
+ rows: rows.map((r) => cols.map((c) => exportCellOf(c, r))),
93
+ };
94
+ }
95
+ export function exportFileBase(name, now = new Date()) {
96
+ const base = trimChar(name.trim().replace(/[\\/:*?"<>|\s]+/g, '_'), '_') || 'export';
97
+ return `${base}_${formatFileStamp(now)}`;
98
+ }
57
99
  export const MIN_COLUMN_WIDTH = 60;
58
100
  export const MAX_COLUMN_WIDTH = 1600;
59
101
  const KEYBOARD_RESIZE_STEP = 16;
@@ -138,7 +180,7 @@ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
138
180
  for (const label of labels) {
139
181
  const childRows = buckets.get(label);
140
182
  const key = parentKey ? `${parentKey}${GROUP_KEY_SEP}${label}` : label;
141
- const expanded = expandedGroups.has(key);
183
+ const expanded = expandedGroups === 'all' || expandedGroups.has(key);
142
184
  out.push({
143
185
  kind: 'group',
144
186
  key,
@@ -178,7 +220,7 @@ export function collectAllGroupKeys(rows, groupBy, columnsById) {
178
220
  recurse(rows, 0, '');
179
221
  return keys;
180
222
  }
181
- export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, }) {
223
+ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, exportable = true, exportFileName, }) {
182
224
  const defaultPrefs = useMemo(() => defaultPrefsFor(columns, initialPrefs), [columns, initialPrefs]);
183
225
  const [prefs, setPrefs, reset] = useViewPreferences(listId, defaultPrefs, persistence);
184
226
  const isMobile = useIsMobile();
@@ -375,7 +417,22 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
375
417
  }
376
418
  setPrefs({ ...prefs, filters });
377
419
  };
378
- const gearButton = (_jsx(ColumnInventoryButton, { columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy }));
420
+ const runExport = (format) => {
421
+ const exportRows = buildRenderItems(sortedFilteredRows, groupBy, columnsById, 'all')
422
+ .filter((i) => i.kind === 'row')
423
+ .map((i) => i.row);
424
+ const table = buildExportTable(exportRows, visibleColumns);
425
+ const base = exportFileBase(exportFileName ?? listId);
426
+ if (format === 'csv')
427
+ downloadFile(toCsv(table), `${base}.csv`, 'text/csv;charset=utf-8');
428
+ else
429
+ downloadFile(toXlsx(table, exportFileName ?? 'Export'), `${base}.xlsx`, XLSX_MIME);
430
+ };
431
+ const gearButton = (_jsx(ColumnInventoryButton, { exportInfo: exportable ? {
432
+ rowCount: sortedFilteredRows.length,
433
+ hasColumns: visibleColumns.some((c) => c.exportable !== false),
434
+ run: runExport,
435
+ } : undefined, columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy }));
379
436
  return (_jsxs("div", { ref: containerRef, className: "border border-[var(--color-border)] overflow-x-auto", style: { '--dt-cols': gridTemplate }, children: [selection && selection.selected.size > 0 && (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-sm border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [_jsxs("span", { children: [selection.selected.size, " ausgew\u00E4hlt"] }), _jsx("button", { type: "button", className: "text-xs underline text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]", onClick: () => selection.onChange(new Set()), children: "Auswahl l\u00F6schen" }), _jsx("div", { className: "flex-1" }), selection.bulkActions] })), isMobile ? (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [selection && (_jsxs("label", { className: "flex items-center gap-2 py-2 pr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "w-5 h-5", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" }), _jsx("span", { className: "text-[var(--color-text-muted)]", children: "Alle" })] })), _jsx("div", { className: "flex-1" }), gearButton] })) : (_jsxs("div", { className: "grid items-center min-w-min px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { gridTemplateColumns: 'var(--dt-cols)', background: 'var(--color-surface-raised)' }, children: [selection && (_jsx("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" })), visibleColumns.map((col) => {
380
437
  const activeSort = prefs.sort?.columnId === col.id ? prefs.sort.direction : null;
381
438
  const filterValue = prefs.filters[col.id];
@@ -569,7 +626,7 @@ function FilterEditor({ column, value, onChange, }) {
569
626
  }
570
627
  return null;
571
628
  }
572
- function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, reset, groupBy, addGroupBy, removeGroupBy, moveGroupBy, clearGroupBy, }) {
629
+ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, reset, groupBy, addGroupBy, removeGroupBy, moveGroupBy, clearGroupBy, exportInfo, }) {
573
630
  const groupableCandidates = columns.filter((c) => !groupBy.includes(c.id));
574
631
  const [open, setOpen] = useState(false);
575
632
  const orderedColumns = useMemo(() => {
@@ -587,5 +644,5 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
587
644
  }, className: "flex-1 px-1 py-0.5 text-xs border border-[var(--color-border)] bg-[var(--color-surface)]", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("option", { value: "", children: "\u2013 w\u00E4hlen \u2013" }), groupableCandidates.map((c) => (_jsx("option", { value: c.id, children: c.label }, c.id)))] })] })), _jsx("div", { className: "border-t border-[var(--color-border)] mb-2" }), _jsx("div", { className: "text-xs font-semibold mb-2 text-[var(--color-text-muted)] uppercase", children: "Spalten" }), orderedColumns.map((col, idx) => {
588
645
  const visible = isColumnVisible(col, prefs.columnVisibility);
589
646
  return (_jsxs("div", { className: "flex items-center gap-2 px-1 py-1 text-sm", onClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", checked: visible, onChange: () => setColumnVisible(col.id, !visible), "aria-label": `Spalte ${col.label} ${visible ? 'ausblenden' : 'einblenden'}` }), _jsx("span", { className: "flex-1 text-xs", children: col.label }), _jsx("button", { type: "button", disabled: idx === 0, onClick: () => moveColumn(col.id, -1), className: "p-2 sm:p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach oben", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowUp, { className: "w-3 h-3" }) }), _jsx("button", { type: "button", disabled: idx === orderedColumns.length - 1, onClick: () => moveColumn(col.id, 1), className: "p-2 sm:p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach unten", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowDown, { className: "w-3 h-3" }) })] }, col.id));
590
- }), _jsx("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: _jsxs(Button, { variant: "ghost", onClick: () => { reset(); setOpen(false); }, children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Zur\u00FCcksetzen"] }) })] })] }));
647
+ }), exportInfo && (_jsxs("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: [_jsx("div", { className: "text-xs font-semibold mb-1 text-[var(--color-text-muted)] uppercase", children: "Exportieren" }), _jsxs("div", { className: "text-xs text-[var(--color-text-muted)] px-1 mb-1", children: ["Aktuelle Ansicht \u00B7 ", exportInfo.rowCount, " ", exportInfo.rowCount === 1 ? 'Zeile' : 'Zeilen'] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs(Button, { variant: "ghost", disabled: exportInfo.rowCount === 0 || !exportInfo.hasColumns, onClick: () => { exportInfo.run('xlsx'); setOpen(false); }, children: [_jsx(Download, { className: "w-4 h-4" }), "Excel"] }), _jsxs(Button, { variant: "ghost", disabled: exportInfo.rowCount === 0 || !exportInfo.hasColumns, title: "CSV, Trennzeichen Semikolon", onClick: () => { exportInfo.run('csv'); setOpen(false); }, children: [_jsx(Download, { className: "w-4 h-4" }), "CSV"] })] })] })), _jsx("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: _jsxs(Button, { variant: "ghost", onClick: () => { reset(); setOpen(false); }, children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Zur\u00FCcksetzen"] }) })] })] }));
591
648
  }
@@ -10,3 +10,4 @@ export { Skeleton, SkeletonRow } from './Skeleton.js';
10
10
  export { RecordDialog, type RecordDialogMode } from './RecordDialog.js';
11
11
  export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
12
12
  export { DataTable, DEFAULT_VIEW_VERSION, type ColumnDef, type FilterDef, type DataTableSelectionProps, } from './DataTable.js';
13
+ export { toCsv, toXlsx, downloadFile, type ExportCell, type ExportTable } from './tableExport.js';
@@ -10,3 +10,4 @@ export { Skeleton, SkeletonRow } from './Skeleton.js';
10
10
  export { RecordDialog } from './RecordDialog.js';
11
11
  export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
12
12
  export { DataTable, DEFAULT_VIEW_VERSION, } from './DataTable.js';
13
+ export { toCsv, toXlsx, downloadFile } from './tableExport.js';
@@ -0,0 +1,22 @@
1
+ export type ExportCell = null | string | number | {
2
+ date: Date;
3
+ withTime: boolean;
4
+ };
5
+ export interface ExportTable {
6
+ headers: string[];
7
+ rows: ExportCell[][];
8
+ }
9
+ export declare const CSV_SEPARATOR = ";";
10
+ export declare function toCsv(table: ExportTable): string;
11
+ export declare function columnLetter(index: number): string;
12
+ export declare function excelSerial(date: Date, withTime: boolean): number;
13
+ export declare function trimChar(s: string, ch: string): string;
14
+ export declare function sanitizeSheetName(name: string): string;
15
+ export declare function toXlsx(table: ExportTable, sheetName?: string): Uint8Array<ArrayBuffer>;
16
+ export declare function crc32(bytes: Uint8Array): number;
17
+ export declare function zipStore(files: Array<{
18
+ name: string;
19
+ data: string;
20
+ }>): Uint8Array<ArrayBuffer>;
21
+ export declare const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
22
+ export declare function downloadFile(data: BlobPart, fileName: string, mime: string): void;
@@ -0,0 +1,254 @@
1
+ import { formatDate, formatDateTime } from '../format.js';
2
+ export const CSV_SEPARATOR = ';';
3
+ function neutralizeFormula(text) {
4
+ return /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
5
+ }
6
+ function csvField(cell) {
7
+ let text;
8
+ if (cell == null)
9
+ text = '';
10
+ else if (typeof cell === 'number')
11
+ text = String(cell).replace('.', ',');
12
+ else if (typeof cell === 'string')
13
+ text = neutralizeFormula(cell);
14
+ else
15
+ text = cell.withTime ? formatDateTime(cell.date) : formatDate(cell.date);
16
+ return /[";\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
17
+ }
18
+ export function toCsv(table) {
19
+ const lines = [table.headers.map(csvField), ...table.rows.map((r) => r.map(csvField))];
20
+ return '' + lines.map((l) => l.join(CSV_SEPARATOR)).join('\r\n') + '\r\n';
21
+ }
22
+ const XLSX_MAX_CELL_CHARS = 32767;
23
+ function xmlEscape(text) {
24
+ return text
25
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
26
+ .replace(/&/g, '&amp;')
27
+ .replace(/</g, '&lt;')
28
+ .replace(/>/g, '&gt;')
29
+ .replace(/"/g, '&quot;');
30
+ }
31
+ export function columnLetter(index) {
32
+ let n = index + 1;
33
+ let s = '';
34
+ while (n > 0) {
35
+ const r = (n - 1) % 26;
36
+ s = String.fromCharCode(65 + r) + s;
37
+ n = Math.floor((n - 1) / 26);
38
+ }
39
+ return s;
40
+ }
41
+ export function excelSerial(date, withTime) {
42
+ const localMs = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), withTime ? date.getHours() : 0, withTime ? date.getMinutes() : 0, withTime ? date.getSeconds() : 0);
43
+ return localMs / 86400000 + 25569;
44
+ }
45
+ const STYLE_HEADER = 1;
46
+ const STYLE_DATE = 2;
47
+ const STYLE_DATETIME = 3;
48
+ const STYLES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
49
+ <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
50
+ <numFmts count="2"><numFmt numFmtId="164" formatCode="dd.mm.yyyy"/><numFmt numFmtId="165" formatCode="dd.mm.yyyy hh:mm"/></numFmts>
51
+ <fonts count="2"><font><sz val="11"/><name val="Calibri"/><family val="2"/></font><font><b/><sz val="11"/><name val="Calibri"/><family val="2"/></font></fonts>
52
+ <fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFE7E7E7"/><bgColor indexed="64"/></patternFill></fill></fills>
53
+ <borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"><color rgb="FFBFBFBF"/></left><right style="thin"><color rgb="FFBFBFBF"/></right><top style="thin"><color rgb="FFBFBFBF"/></top><bottom style="medium"><color rgb="FF808080"/></bottom><diagonal/></border></borders>
54
+ <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
55
+ <cellXfs count="4">
56
+ <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
57
+ <xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="center"/></xf>
58
+ <xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
59
+ <xf numFmtId="165" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
60
+ </cellXfs>
61
+ <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
62
+ </styleSheet>`;
63
+ const CONTENT_TYPES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
64
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
65
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
66
+ <Default Extension="xml" ContentType="application/xml"/>
67
+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
68
+ <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
69
+ <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
70
+ </Types>`;
71
+ const ROOT_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
72
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
73
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
74
+ </Relationships>`;
75
+ const WORKBOOK_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
76
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
77
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
78
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
79
+ </Relationships>`;
80
+ export function trimChar(s, ch) {
81
+ let start = 0;
82
+ let end = s.length;
83
+ while (start < end && s[start] === ch)
84
+ start++;
85
+ while (end > start && s[end - 1] === ch)
86
+ end--;
87
+ return s.slice(start, end);
88
+ }
89
+ export function sanitizeSheetName(name) {
90
+ const cleaned = trimChar(name.replace(/[[\]:*?/\\]/g, ' '), "'").trim().slice(0, 31).trim();
91
+ return cleaned || 'Export';
92
+ }
93
+ function textCell(ref, text, style) {
94
+ const s = style != null ? ` s="${style}"` : '';
95
+ return `<c r="${ref}" t="inlineStr"${s}><is><t xml:space="preserve">${xmlEscape(text.slice(0, XLSX_MAX_CELL_CHARS))}</t></is></c>`;
96
+ }
97
+ function dataCell(ref, cell) {
98
+ if (cell == null)
99
+ return '';
100
+ if (typeof cell === 'number')
101
+ return `<c r="${ref}"><v>${cell}</v></c>`;
102
+ if (typeof cell === 'string')
103
+ return cell === '' ? '' : textCell(ref, cell);
104
+ const serial = excelSerial(cell.date, cell.withTime);
105
+ if (serial < 61)
106
+ return textCell(ref, cell.withTime ? formatDateTime(cell.date) : formatDate(cell.date));
107
+ return `<c r="${ref}" s="${cell.withTime ? STYLE_DATETIME : STYLE_DATE}"><v>${serial}</v></c>`;
108
+ }
109
+ function displayLength(cell) {
110
+ if (cell == null)
111
+ return 0;
112
+ if (typeof cell === 'number')
113
+ return String(cell).length;
114
+ if (typeof cell === 'string')
115
+ return Math.max(...cell.split('\n').map((l) => l.length));
116
+ return cell.withTime ? 16 : 10;
117
+ }
118
+ function sheetXml(table) {
119
+ const colCount = Math.max(table.headers.length, 1);
120
+ const lastCol = columnLetter(colCount - 1);
121
+ const lastRow = table.rows.length + 1;
122
+ const widths = table.headers.map((h, ci) => {
123
+ let max = h.length + 3;
124
+ for (const r of table.rows)
125
+ max = Math.max(max, displayLength(r[ci] ?? null));
126
+ return Math.min(60, Math.max(8, max + 2));
127
+ });
128
+ const header = `<row r="1">${table.headers.map((h, ci) => textCell(`${columnLetter(ci)}1`, h, STYLE_HEADER)).join('')}</row>`;
129
+ const body = table.rows.map((r, ri) => {
130
+ const rowNo = ri + 2;
131
+ return `<row r="${rowNo}">${r.map((c, ci) => dataCell(`${columnLetter(ci)}${rowNo}`, c)).join('')}</row>`;
132
+ }).join('');
133
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
134
+ <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
135
+ <dimension ref="A1:${lastCol}${lastRow}"/>
136
+ <sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>
137
+ <sheetFormatPr defaultRowHeight="15"/>
138
+ ${widths.length > 0 ? `<cols>${widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join('')}</cols>` : ''}
139
+ <sheetData>${header}${body}</sheetData>
140
+ ${table.headers.length > 0 ? `<autoFilter ref="A1:${lastCol}${lastRow}"/>` : ''}
141
+ </worksheet>`;
142
+ }
143
+ function workbookXml(sheetName, table) {
144
+ const lastCol = columnLetter(Math.max(table.headers.length, 1) - 1);
145
+ const quoted = `'${sheetName.replace(/'/g, "''")}'`;
146
+ const filterName = table.headers.length > 0
147
+ ? `<definedNames><definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">${xmlEscape(quoted)}!$A$1:$${lastCol}$${table.rows.length + 1}</definedName></definedNames>`
148
+ : '';
149
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
150
+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
151
+ <sheets><sheet name="${xmlEscape(sheetName)}" sheetId="1" r:id="rId1"/></sheets>
152
+ ${filterName}
153
+ </workbook>`;
154
+ }
155
+ export function toXlsx(table, sheetName = 'Export') {
156
+ const name = sanitizeSheetName(sheetName);
157
+ return zipStore([
158
+ { name: '[Content_Types].xml', data: CONTENT_TYPES_XML },
159
+ { name: '_rels/.rels', data: ROOT_RELS_XML },
160
+ { name: 'xl/workbook.xml', data: workbookXml(name, table) },
161
+ { name: 'xl/_rels/workbook.xml.rels', data: WORKBOOK_RELS_XML },
162
+ { name: 'xl/styles.xml', data: STYLES_XML },
163
+ { name: 'xl/worksheets/sheet1.xml', data: sheetXml(table) },
164
+ ]);
165
+ }
166
+ let crcTable = null;
167
+ export function crc32(bytes) {
168
+ if (!crcTable) {
169
+ crcTable = new Uint32Array(256);
170
+ for (let n = 0; n < 256; n++) {
171
+ let c = n;
172
+ for (let k = 0; k < 8; k++)
173
+ c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
174
+ crcTable[n] = c >>> 0;
175
+ }
176
+ }
177
+ let crc = 0xFFFFFFFF;
178
+ for (let i = 0; i < bytes.length; i++)
179
+ crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
180
+ return (crc ^ 0xFFFFFFFF) >>> 0;
181
+ }
182
+ export function zipStore(files) {
183
+ const enc = new TextEncoder();
184
+ const dosTime = 0;
185
+ const dosDate = ((2026 - 1980) << 9) | (1 << 5) | 1;
186
+ const locals = [];
187
+ const centrals = [];
188
+ let offset = 0;
189
+ for (const f of files) {
190
+ const name = enc.encode(f.name);
191
+ const data = enc.encode(f.data);
192
+ const crc = crc32(data);
193
+ const local = new Uint8Array(30 + name.length + data.length);
194
+ const lv = new DataView(local.buffer);
195
+ lv.setUint32(0, 0x04034B50, true);
196
+ lv.setUint16(4, 20, true);
197
+ lv.setUint16(6, 0x0800, true);
198
+ lv.setUint16(8, 0, true);
199
+ lv.setUint16(10, dosTime, true);
200
+ lv.setUint16(12, dosDate, true);
201
+ lv.setUint32(14, crc, true);
202
+ lv.setUint32(18, data.length, true);
203
+ lv.setUint32(22, data.length, true);
204
+ lv.setUint16(26, name.length, true);
205
+ lv.setUint16(28, 0, true);
206
+ local.set(name, 30);
207
+ local.set(data, 30 + name.length);
208
+ locals.push(local);
209
+ const central = new Uint8Array(46 + name.length);
210
+ const cv = new DataView(central.buffer);
211
+ cv.setUint32(0, 0x02014B50, true);
212
+ cv.setUint16(4, 20, true);
213
+ cv.setUint16(6, 20, true);
214
+ cv.setUint16(8, 0x0800, true);
215
+ cv.setUint16(10, 0, true);
216
+ cv.setUint16(12, dosTime, true);
217
+ cv.setUint16(14, dosDate, true);
218
+ cv.setUint32(16, crc, true);
219
+ cv.setUint32(20, data.length, true);
220
+ cv.setUint32(24, data.length, true);
221
+ cv.setUint16(28, name.length, true);
222
+ cv.setUint32(42, offset, true);
223
+ central.set(name, 46);
224
+ centrals.push(central);
225
+ offset += local.length;
226
+ }
227
+ const centralSize = centrals.reduce((s, c) => s + c.length, 0);
228
+ const end = new Uint8Array(22);
229
+ const ev = new DataView(end.buffer);
230
+ ev.setUint32(0, 0x06054B50, true);
231
+ ev.setUint16(8, files.length, true);
232
+ ev.setUint16(10, files.length, true);
233
+ ev.setUint32(12, centralSize, true);
234
+ ev.setUint32(16, offset, true);
235
+ const out = new Uint8Array(offset + centralSize + end.length);
236
+ let pos = 0;
237
+ for (const part of [...locals, ...centrals, end]) {
238
+ out.set(part, pos);
239
+ pos += part.length;
240
+ }
241
+ return out;
242
+ }
243
+ export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
244
+ export function downloadFile(data, fileName, mime) {
245
+ const url = URL.createObjectURL(new Blob([data], { type: mime }));
246
+ const a = document.createElement('a');
247
+ a.href = url;
248
+ a.download = fileName;
249
+ a.style.display = 'none';
250
+ document.body.appendChild(a);
251
+ a.click();
252
+ a.remove();
253
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
254
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efa-one/sdk",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Offizielles SDK für efa-one-Apps: Auth/Session, Service-Discovery, Gateway-Client, Audit/Reporting/Mail/Notifications, Permission- und Capability-Registrierung sowie das Frontend-IPC-/i18n-Toolkit.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -122,7 +122,7 @@
122
122
  "eslint-plugin-react-hooks": "^7.1.1",
123
123
  "express": "^4.19.2",
124
124
  "i18next": "^23.11.0",
125
- "i18next-http-backend": "^3.0.5",
125
+ "i18next-http-backend": "^4.0.2",
126
126
  "jsonwebtoken": "^9.0.2",
127
127
  "lucide-react": "^0.577.0",
128
128
  "pg": "^8.12.0",