@xiriframework/xiri-ng 0.2.26 → 0.2.28

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.
@@ -0,0 +1,130 @@
1
+ # xiri-dyncomponent — JSON-Renderer
2
+
3
+ Die `xiri-dyncomponent` ist der Kern des JSON-Driven-UI Patterns: ein Array von `XiriDynData`-Objekten wird zu einer Kette von xiri-Komponenten gerendert.
4
+
5
+ ## Selector
6
+
7
+ `xiri-dyncomponent`
8
+
9
+ ## Inputs
10
+
11
+ ```typescript
12
+ @input.required data: XiriDynData[];
13
+ @input filterData: any = undefined;
14
+ @input dyncomponent: TemplateRef<any> = undefined;
15
+ // Optional: TemplateRef für Custom-Rendering (type: 'custom' oder unbekannter type).
16
+ ```
17
+
18
+ ## Outputs
19
+
20
+ Keine.
21
+
22
+ ## XiriDynData Interface
23
+
24
+ ```typescript
25
+ export interface XiriDynData {
26
+ id?: number; // Wird auto-generiert wenn fehlt.
27
+ type: XiriDynDataType | (string & {}); // Komponenten-Typ.
28
+ data?: any; // Settings-Objekt der Ziel-Komponente.
29
+ display?: string; // CSS-Klassen für Wrapper.
30
+ newRow?: boolean; // Grid-Layout: neue Zeile beginnen.
31
+ mode?: string; // Nur für 'barchart': 'simple' | 'stacked' | 'heatmap'
32
+ }
33
+
34
+ export type XiriDynDataType =
35
+ | 'card' | 'buttonline' | 'table' | 'cardlink' | 'links'
36
+ | 'form' | 'query' | 'stepper' | 'header' | 'list'
37
+ | 'spacer' | 'container'
38
+ | 'infopoint' | 'multiprogress' | 'imagetext'
39
+ | 'tabs' | 'expansion' | 'infotext' | 'html'
40
+ | 'stat' | 'empty-state' | 'timeline'
41
+ | 'page-header' | 'section' | 'divider'
42
+ | 'stat-grid' | 'toolbar' | 'description-list'
43
+ | 'barchart' | 'linechart' | 'piechart' | 'gaugechart'
44
+ | 'heatmap' | 'calendar' | 'tree' | 'sankey' | 'gantt';
45
+ ```
46
+
47
+ ## Mapping type → Komponente
48
+
49
+ | `type` | Rendered Component | `data` = Settings-Interface |
50
+ | ------------------- | ---------------------- | ------------------------------------ |
51
+ | `card` | `xiri-card` | `XiriCardSettings` |
52
+ | `buttonline` | `xiri-buttonline` | `XiriButtonlineSettings` |
53
+ | `table` | `xiri-table` | `XiriTableSettings` |
54
+ | `cardlink` | `xiri-cardlink` | `XiriCardlinkSettings` |
55
+ | `links` | `xiri-links` | `XiriLinksSettings` |
56
+ | `form` | `xiri-form` | `XiriFormSettings` |
57
+ | `query` | `xiri-query` | `XiriQuerySettings` |
58
+ | `stepper` | `xiri-stepper` | `XiriStepperSettings` |
59
+ | `header` | `xiri-header` | `XiriHeaderSettings` |
60
+ | `list` | `xiri-list` | `XiriListSettings` |
61
+ | `infopoint` | `xiri-infopoint` | `XiriInfopointSettings` |
62
+ | `multiprogress` | `xiri-multiprogress` | `XiriMultiprogressSettings` |
63
+ | `imagetext` | `xiri-imagetext` | `XiriImagetextSettings` |
64
+ | `tabs` | `xiri-tabs` | `XiriTabsSettings` |
65
+ | `expansion` | `xiri-expansion` | `XiriExpansionSettings` |
66
+ | `stat` | `xiri-stat` | `XiriStatSettings` |
67
+ | `empty-state` | `xiri-empty-state` | `XiriEmptyStateSettings` |
68
+ | `timeline` | `xiri-timeline` | `XiriTimelineSettings` |
69
+ | `page-header` | `xiri-page-header` | `XiriPageHeaderSettings` |
70
+ | `section` | `xiri-section` | `XiriSectionSettings` (rekursiv!) |
71
+ | `divider` | `xiri-divider` | `XiriDividerSettings` |
72
+ | `stat-grid` | `xiri-stat-grid` | `XiriStatGridSettings` |
73
+ | `toolbar` | `xiri-toolbar` | `XiriToolbarSettings` |
74
+ | `description-list` | `xiri-description-list`| `XiriDescriptionListSettings` |
75
+ | `barchart` | `xiri-barchart` | `XiriBarChartSettings` (+ `mode` auf XiriDynData) |
76
+ | `linechart` | `xiri-linechart` | `XiriLineChartSettings` |
77
+ | `piechart` | `xiri-piechart` | `XiriPieChartSettings` |
78
+ | `gaugechart` | `xiri-gaugechart` | `XiriGaugeChartSettings` |
79
+ | `heatmap` | `xiri-heatmap` | `XiriHeatmapSettings` |
80
+ | `calendar` | `xiri-calendar` | `XiriCalendarSettings` |
81
+ | `tree` | `xiri-tree` | `XiriTreeSettings` |
82
+ | `sankey` | `xiri-sankey` | `XiriSankeySettings` |
83
+ | `gantt` | `xiri-gantt` | `XiriGanttSettings` |
84
+ | `spacer` | vertical spacer div | `{ size?: string }` |
85
+ | `container` | wrapper div | `{ components: XiriDynData[] }` |
86
+ | `infotext` | Text-Absatz | `{ text: string }` |
87
+ | `html` | innerHTML (SafeHtml) | `{ html: string }` |
88
+
89
+ ## Custom-Rendering via TemplateRef
90
+
91
+ Für Typen außerhalb der Liste: übergib einen `dyncomponent` TemplateRef als Fallback.
92
+
93
+ ```html
94
+ <xiri-dyncomponent [data]="components" [dyncomponent]="customTpl"/>
95
+
96
+ <ng-template #customTpl let-item>
97
+ <app-my-custom-widget [data]="item.data"/>
98
+ </ng-template>
99
+ ```
100
+
101
+ Der TemplateRef erhält das gesamte `XiriDynData`-Item als Context (`$implicit`).
102
+
103
+ ## Verschachtelung
104
+
105
+ `section`, `tabs`, `expansion` und `container` akzeptieren in ihren `data`-Feldern wieder `XiriDynData[]` (rekursiv) — die `xiri-dyncomponent` rendert alle Ebenen durch.
106
+
107
+ ```typescript
108
+ const page: XiriDynData[] = [
109
+ { type: 'page-header', data: { title: 'Dashboard' } },
110
+ { type: 'stat-grid', data: { stats: [...], columns: 4 } },
111
+ { type: 'section', data: {
112
+ title: 'Details',
113
+ collapsible: true,
114
+ components: [
115
+ { type: 'table', data: { url: '/api/items' } },
116
+ { type: 'divider', data: {} },
117
+ { type: 'infopoint', data: { text: 'Hinweis', icon: 'info' } },
118
+ ],
119
+ }},
120
+ ];
121
+ ```
122
+
123
+ ## filterData — Weitergabe an Tabellen und Buttons
124
+
125
+ `filterData` wird automatisch an alle `xiri-table`-, `xiri-buttonline`- und `xiri-toolbar`-Kinder weitergereicht. Das ist wichtig, wenn ein `xiri-query` Filter-Werte setzt, die Tabellen oder API-Buttons konsumieren sollen.
126
+
127
+ ```html
128
+ <xiri-query [settings]="qs" (change)="filter = $event"></xiri-query>
129
+ <xiri-dyncomponent [data]="results" [filterData]="filter"/>
130
+ ```
@@ -0,0 +1,282 @@
1
+ # Form-Fields — xiri-ng Reference
2
+
3
+ ## XiriFormFieldsComponent
4
+
5
+ Renderer für `XiriFormField[]`. Baut intern eine `UntypedFormGroup` mit Reactive Validators, handhabt `showWhen`, collapsible Headers und alle 19 Feldtypen.
6
+
7
+ ### Selector
8
+
9
+ `xiri-form-fields`
10
+
11
+ ### Inputs
12
+
13
+ ```typescript
14
+ @input form: XiriFormField[] | null = null
15
+ @input display: XiriFormFieldDisplay = 'full' // 'full' | 'line' | 'small'
16
+ @input disabled: boolean = false
17
+ @input check: Observable<void> = null // Trigger für Revalidierung
18
+ ```
19
+
20
+ ### Outputs
21
+
22
+ ```typescript
23
+ @output formChange: EventEmitter<any>
24
+ // Emittet die UntypedFormGroup (nicht nur values) bei Änderungen.
25
+ ```
26
+
27
+ ### Öffentliche API
28
+
29
+ ```typescript
30
+ formGroup: UntypedFormGroup // Reactive Form
31
+
32
+ isFieldVisible(field: XiriFormField): boolean
33
+ toggleSection(header: XiriFormField): void
34
+ isSectionCollapsed(headerId: string): boolean
35
+ ```
36
+
37
+ ### Zugriff auf Werte und Validität
38
+
39
+ ```typescript
40
+ @ViewChild(XiriFormFieldsComponent) fieldsCmp!: XiriFormFieldsComponent;
41
+
42
+ submit() {
43
+ if (this.fieldsCmp.formGroup.invalid) {
44
+ this.fieldsCmp.formGroup.markAllAsTouched();
45
+ return;
46
+ }
47
+ const payload = this.fieldsCmp.formGroup.value;
48
+ this.data.post('/api/save', payload)
49
+ .subscribe(res => this.responseHandler.handle(res));
50
+ }
51
+ ```
52
+
53
+ ## XiriFormField Interface
54
+
55
+ ```typescript
56
+ export interface XiriFormField {
57
+ // --- Basis ---
58
+ id: string;
59
+ type: string; // siehe Liste unten
60
+ subtype?: string; // z.B. 'email' subtype von 'text'
61
+ formtype?: string; // Alt-Weg, gleich wie type
62
+
63
+ // --- Label & Display ---
64
+ name?: string; // Label
65
+ hint?: string;
66
+ class?: string;
67
+ textPrefix?: string;
68
+ textSuffix?: string;
69
+ iconPrefix?: string;
70
+ iconSuffix?: string;
71
+ locale?: string;
72
+ placeholder?: string; // v.a. für bool / header
73
+
74
+ // --- Wert & Validierung ---
75
+ value?: any;
76
+ validations?: XiriFormValidator[];
77
+ list?: XiriFormFieldSelectOption[]; // für select/treeselect
78
+ texts?: object; // für timelimit
79
+
80
+ // --- State ---
81
+ hide?: boolean;
82
+ required?: boolean;
83
+ disabled?: boolean;
84
+ collapsible?: boolean; // header
85
+ collapsed?: boolean; // header initial
86
+
87
+ // --- Constraints ---
88
+ min?: number; // number | date | array-length
89
+ max?: number;
90
+ pattern?: string; // regex
91
+
92
+ // --- Select / Model / Treeselect ---
93
+ multiple?: boolean;
94
+ url?: string; // load options von Backend
95
+ search?: boolean;
96
+ serverSideSearch?: boolean;
97
+ params?: object;
98
+
99
+ // --- File ---
100
+ accept?: string;
101
+ pwdhide?: boolean; // password
102
+
103
+ // --- Question / Waiting ---
104
+ icon?: string;
105
+ iconColor?: XiriColor;
106
+ done?: boolean;
107
+
108
+ // --- Conditional Display ---
109
+ showWhen?: XiriFormFieldCondition | XiriFormFieldCondition[];
110
+
111
+ // --- Weitere ---
112
+ rows?: number; // textarea
113
+ array?: any[]; // alt zu list
114
+ tree?: boolean;
115
+ control?: FormControl; // Internal
116
+ }
117
+ ```
118
+
119
+ ### Feld-Typen
120
+
121
+ | `type` | Rendered als | Wichtige Properties |
122
+ | --------------- | ------------------------------------------------- | ---------------------------------- |
123
+ | `text` | Mat-Input | `subtype` (email/url), `pattern` |
124
+ | `email` | Mat-Input type=email | |
125
+ | `password` | Mat-Input type=password | `pwdhide` |
126
+ | `textarea` | Mat-Textarea | `rows` |
127
+ | `number` | Mat-Input type=number | `min`, `max`, `textSuffix` |
128
+ | `bool` | Mat-Checkbox / Slide-Toggle | `placeholder` für Begleittext |
129
+ | `select` | Mat-Select mit `list` | `multiple`, `search` |
130
+ | `multiselect` | Mat-Select mit `multiple: true` | `list` oder `url` |
131
+ | `model` | Mat-Select mit Backend-Load | `url`, `serverSideSearch` |
132
+ | `object` | Komplexes Objekt-Select (JSON-Value) | `url`, `list` |
133
+ | `treeselect` | Tree-Picker | `url`, `tree: true` |
134
+ | `date` | Mat-Datepicker (Unix-Timestamp) | `min`, `max` |
135
+ | `datetime` | Date + Time Picker | `min`, `max` |
136
+ | `daterange` | Start- + End-Date | `min`, `max` |
137
+ | `datetimerange` | Start- + End-DateTime | |
138
+ | `yearmonth` | Monats-Picker (Multi-Year-View, MM.yyyy) | `min`, `max` (Unix), `required` |
139
+ | `file` | File-Upload Button | `accept` |
140
+ | `volume` | Volume-Slider (mit Einheit via `textSuffix`) | `min`, `max` |
141
+ | `timelimit` | Time-Limit Auswahl | `texts` (Label-Overrides) |
142
+ | `chips` | Mat-Chip-List | `list`, `validations` |
143
+ | `question` | Read-Only Frage + Icon (z.B. Confirmation-Dialog) | `icon`, `iconColor`, `done` |
144
+ | `waiting` | Loading-Spinner | |
145
+ | `header` | Sektions-Header (mit optional Collapse) | `collapsible`, `collapsed` |
146
+
147
+ #### `yearmonth` — Monats-Auswahl
148
+
149
+ Render: eigener `<xiri-yearmonth>`, intern Mat-Datepicker mit `startView="multi-year"`.
150
+ Value wird auf den **1. des Monats 00:00 (lokal)** normalisiert und als Unix-Timestamp
151
+ (Sekunden) zurückgegeben. Anzeige-Format `MM.yyyy`, Picker-Labels `MMM yyyy` /
152
+ `MMMM yyyy`. Default `required: true`, wenn nicht explizit gesetzt.
153
+
154
+ ```typescript
155
+ { id: 'period', type: 'yearmonth', name: 'Berichtsmonat',
156
+ required: true,
157
+ min: <unixSeconds>, // optional, untere Schranke
158
+ max: <unixSeconds> // optional, obere Schranke
159
+ }
160
+ ```
161
+
162
+ Backend-Pendant: `field.NewYearMonthField(...)` (siehe xiri-go-expert).
163
+
164
+ ### showWhen — Conditional Visibility
165
+
166
+ ```typescript
167
+ export interface XiriFormFieldCondition {
168
+ field: string;
169
+ operator: 'equals' | 'notEquals' | 'contains'
170
+ | 'greaterThan' | 'lessThan' | 'in' | 'notEmpty';
171
+ value?: any;
172
+ }
173
+ ```
174
+
175
+ Einzelne Bedingung:
176
+
177
+ ```typescript
178
+ { id: 'reason', type: 'text', name: 'Grund',
179
+ showWhen: { field: 'active', operator: 'equals', value: false } }
180
+ ```
181
+
182
+ Array = **UND**-Verknüpfung:
183
+
184
+ ```typescript
185
+ { id: 'priorityNote', type: 'textarea', name: 'Prio-Notiz',
186
+ showWhen: [
187
+ { field: 'priority', operator: 'in', value: ['high', 'critical'] },
188
+ { field: 'note', operator: 'notEmpty' },
189
+ ]}
190
+ ```
191
+
192
+ ### Select-Optionen
193
+
194
+ ```typescript
195
+ export interface XiriFormFieldSelectOption {
196
+ id: number;
197
+ name: string;
198
+ disabled?: boolean;
199
+ color?: XiriColor;
200
+ }
201
+ ```
202
+
203
+ Inline (statisch):
204
+
205
+ ```typescript
206
+ { id: 'status', type: 'select', name: 'Status',
207
+ list: [
208
+ { id: 1, name: 'Aktiv', color: 'success' },
209
+ { id: 2, name: 'Inaktiv', color: 'warn' },
210
+ { id: 3, name: 'Gesperrt', color: 'error', disabled: true },
211
+ ]}
212
+ ```
213
+
214
+ Per Backend (`model` oder `select` mit `url`):
215
+
216
+ ```typescript
217
+ { id: 'group', type: 'model', name: 'Gruppe',
218
+ url: '/api/groups/options',
219
+ serverSideSearch: true,
220
+ params: { tenant: 42 } }
221
+ ```
222
+
223
+ Das Backend liefert `XiriFormFieldSelectOption[]` zurück.
224
+
225
+ ## XiriFormValidator
226
+
227
+ ```typescript
228
+ export interface XiriFormValidator {
229
+ type: 'required' | 'minlength' | 'maxlength' | 'pattern' |
230
+ 'min' | 'max' | 'email' | ...;
231
+ value?: any;
232
+ message?: string;
233
+ }
234
+ ```
235
+
236
+ Beispiel:
237
+
238
+ ```typescript
239
+ { id: 'pwd', type: 'password', name: 'Passwort',
240
+ validations: [
241
+ { type: 'required' },
242
+ { type: 'minlength', value: 8, message: 'Mind. 8 Zeichen' },
243
+ { type: 'pattern', value: '.*[0-9].*', message: '≥1 Zahl' },
244
+ ]}
245
+ ```
246
+
247
+ ## XiriSelectDirective
248
+
249
+ Interne Directive für Mat-Select mit Client- oder Server-side-Search. Wird normalerweise nicht direkt verwendet — `XiriFormFieldsComponent` setzt sie automatisch für `select`, `multiselect`, `model`, `object`.
250
+
251
+ ### Selector
252
+
253
+ `[xiriSelect]` auf `<mat-select>`
254
+
255
+ ### Inputs (mit Alias)
256
+
257
+ ```typescript
258
+ @input({ alias: 'values' })
259
+ values: XiriFormFieldSelectOption[] = [];
260
+
261
+ @input({ alias: 'serverSideSearch' })
262
+ serverSideSearch: boolean = false;
263
+
264
+ @input({ alias: 'serverUrl' })
265
+ serverUrl: string = '';
266
+
267
+ @input({ alias: 'serverParams' })
268
+ serverParams: any = {};
269
+
270
+ @input({ alias: 'predicate' })
271
+ predicate: XiriSelectPredicate = DEFAULT_PREDICATE; // (option, term) => boolean
272
+
273
+ @input({ alias: 'compare' })
274
+ compare: XiriSelectCompare = DEFAULT_COMPARE; // (a, b) => boolean
275
+ ```
276
+
277
+ ### API
278
+
279
+ ```typescript
280
+ get formControl(): UntypedFormControl;
281
+ get filter(): Observable<XiriFormFieldSelectOption[]>;
282
+ ```
@@ -0,0 +1,177 @@
1
+ # Setup & Services — xiri-ng Reference
2
+
3
+ ## provideXiriServices
4
+
5
+ ```typescript
6
+ import { provideXiriServices } from '@xiriframework/xiri-ng';
7
+
8
+ export const appConfig: ApplicationConfig = {
9
+ providers: [
10
+ provideHttpClient(),
11
+ provideXiriServices({ api: '/api/' }),
12
+ ],
13
+ };
14
+ ```
15
+
16
+ Signatur:
17
+
18
+ ```typescript
19
+ function provideXiriServices(config: Partial<XiriDataServiceConfig>): EnvironmentProviders
20
+
21
+ class XiriDataServiceConfig {
22
+ api: string = '/api/';
23
+ }
24
+ ```
25
+
26
+ Alle xiri-Services sind `providedIn: 'root'` — kein manuelles Registrieren nötig.
27
+
28
+ ## XiriDataService
29
+
30
+ HTTP-Client mit automatischer Snackbar-Integration (parst `response.message` + `response.messageType`).
31
+
32
+ ```typescript
33
+ get(url: string): Observable<Object>
34
+ post(url: string, data: any): Observable<any>
35
+
36
+ postFile(url: string, data: any): Observable<any>
37
+ // responseType: 'blob' — gibt rohes Blob zurück
38
+
39
+ postFileResponse(url: string, data: any): Observable<HttpResponse<Blob>>
40
+ // mit Headern (für Filename aus Content-Disposition) — nutze mit XiriDownloadService
41
+
42
+ getConfigApi(): string
43
+ // aktuelle Base-URL (z.B. '/api/')
44
+
45
+ postDownload(url: string, data: any): void
46
+ // DEPRECATED — nutze postFileResponse() + XiriDownloadService.download()
47
+ ```
48
+
49
+ URLs werden mit `getConfigApi()` prefixed, wenn sie nicht absolut sind.
50
+
51
+ ## XiriSnackbarService
52
+
53
+ ```typescript
54
+ success(message: string, duration?: number, action?: string): MatSnackBarRef
55
+ error (message: string, duration?: number, action?: string): MatSnackBarRef
56
+ info (message: string, duration?: number, action?: string): MatSnackBarRef
57
+ warning(message: string, duration?: number, action?: string): MatSnackBarRef
58
+
59
+ handleResponse(response: any): boolean
60
+ // Gibt true zurück wenn response.message existiert und gezeigt wurde.
61
+ // messageType mapt auf success | error | warning | info.
62
+ ```
63
+
64
+ Wird von `XiriDataService` intern aufgerufen — du musst `handleResponse` nur selber aufrufen, wenn du die Response manuell behandelst.
65
+
66
+ ## XiriResponseHandlerService
67
+
68
+ Zentrale Logik für Backend-Responses aus `xiri-go` (`ReturnGoto`, `ReturnRefreshPage`, `ReturnRefreshTable`, `ReturnUpdateTableField`).
69
+
70
+ ```typescript
71
+ handle(result: any, callbacks?: {
72
+ onTableRefresh?: () => void;
73
+ onTableUpdate?: (id: any, field: string, content: any) => void;
74
+ }): void
75
+ ```
76
+
77
+ Verhalten je nach `result.refreshType` / `result.gotoUrl`:
78
+ - Navigation → `Router.navigateByUrl`
79
+ - Page-Refresh → `window.location.reload()`
80
+ - Table-Refresh → ruft `onTableRefresh()` wenn gesetzt
81
+ - Table-Update → ruft `onTableUpdate(id, field, content)` wenn gesetzt
82
+
83
+ ## XiriFormService
84
+
85
+ Orchestriert Form-Fetch/Submit mit State-Persistierung.
86
+
87
+ ```typescript
88
+ get(url: string, data?: any, extra?: any): Observable<XiriFormServiceData>
89
+ // GET wenn data === null, sonst POST. Gibt geparste FormServiceData zurück.
90
+
91
+ parse(res: XiriFormServiceReturn): XiriFormServiceData
92
+ // Parst rohe Backend-Response (type: 'form' | 'question' | 'waiting').
93
+
94
+ loadState(saveStateId: string | null | undefined,
95
+ fields: XiriFormField[]): XiriFormField[]
96
+ // Merged persistierte Values in die Fields (SessionStorage, 3600s Timeout).
97
+
98
+ saveState(saveStateId: string | null | undefined, values: any): void
99
+ // Persistiert in SessionStorage.
100
+ ```
101
+
102
+ ## XiriThemeService (ThemeService)
103
+
104
+ Signal-basiert, System-Präferenz-aware.
105
+
106
+ ```typescript
107
+ readonly mode: Signal<ThemeMode> // 'light' | 'dark' | 'auto'
108
+ readonly isDark: Signal<boolean> // computed
109
+ readonly isLight: Signal<boolean> // computed
110
+
111
+ setTheme(mode: ThemeMode): void // persistiert in localStorage
112
+ toggle(): void // wechselt zwischen light/dark
113
+ resetToAuto(): void // auto = folgt OS
114
+ ```
115
+
116
+ Beispiel in einer Komponente:
117
+
118
+ ```typescript
119
+ private theme = inject(ThemeService);
120
+
121
+ toggleTheme() { this.theme.toggle(); }
122
+ darkMode = computed(() => this.theme.isDark());
123
+ ```
124
+
125
+ ## XiriDownloadService
126
+
127
+ ```typescript
128
+ download(result: HttpResponse<Blob>, filename: string, openInNewTab: boolean): boolean
129
+ // Erstellt blob-URL, triggert Download oder öffnet Tab. True bei Erfolg.
130
+ ```
131
+
132
+ Kombination mit `XiriDataService.postFileResponse`:
133
+
134
+ ```typescript
135
+ this.data.postFileResponse('/api/export', payload).subscribe(res => {
136
+ this.download.download(res, 'export.xlsx', false);
137
+ });
138
+ ```
139
+
140
+ ## XiriDateService
141
+
142
+ Unix-Timestamp ↔ lokale Datums-Strings mit Timezone + Locale.
143
+
144
+ ```typescript
145
+ setTimezone(tz: string): void
146
+ setLocale(localeString: string, locale: Locale): void // date-fns Locale
147
+
148
+ unixToLocal(stime: number): Date | null
149
+ unixToStringDateTime(stime: number): string // 'yyyy-MM-dd HH:mm'
150
+ unixToStringDate(stime: number): string // 'd. LLL.'
151
+ unixToStringDateYear(stime: number): string // 'd. LLL. yy'
152
+ dateToUnix(date: Date): number
153
+ ```
154
+
155
+ Der Backend-`UiContext` schickt Locale/Timezone als Teil der JSON-Responses → Kalibrierung via `setLocale` / `setTimezone` erfolgt typischerweise in einem Auth-/Startup-Flow.
156
+
157
+ ## XiriNumberService
158
+
159
+ ```typescript
160
+ setLocale(locale: string): void // Default: 'de-DE'
161
+ formatNumber(value: number, webformat?: string): string
162
+ // webformat: 'integer' | 'float1' | 'float2' | 'float3' | 'float4'
163
+ ```
164
+
165
+ ## XiriLocalStorageService / XiriSessionStorageService
166
+
167
+ Wrapper mit Timestamp-Metadaten und In-Memory-Fallback (falls localStorage blockiert).
168
+
169
+ ```typescript
170
+ set(name: string, value: any): void // speichert mit Timestamp
171
+ get(name: string): any // value oder null
172
+ getTimeout(name: string, maxSeconds: number): any // null wenn älter als maxSeconds
173
+ remove(name: string): void
174
+ clear(): void
175
+ ```
176
+
177
+ `XiriSessionStorageService` hat dieselbe API, wird v.a. für Form-State und Table-State genutzt (`saveState: true` in Table/Query).