@masterteam/forms 0.0.35 → 0.0.37

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,690 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, signal, computed, input, output, effect, untracked, Component } from '@angular/core';
3
+ import * as i1 from '@angular/forms';
4
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
5
+ import * as i2 from '@angular/common';
6
+ import { CommonModule } from '@angular/common';
7
+ import { DynamicForm } from '@masterteam/forms/dynamic-form';
8
+ import { HttpClient, HttpContext } from '@angular/common/http';
9
+ import { ValidatorConfig, TextFieldConfig, SelectFieldConfig, MultiSelectFieldConfig, UserSearchFieldConfig, REQUEST_CONTEXT, UploadFileFieldConfig, ToggleFieldConfig, DateFieldConfig, SliderFieldConfig, NumberFieldConfig, EditorFieldConfig } from '@masterteam/components';
10
+
11
+ /**
12
+ * Stateless HTTP service for process-forms runtime APIs.
13
+ * Root-provided — safe to share across multiple ClientForm instances.
14
+ */
15
+ class ClientFormApiService {
16
+ http = inject(HttpClient);
17
+ baseUrl = 'process-forms';
18
+ /**
19
+ * Load form configuration and values for a given operation context.
20
+ * Backend determines mode (Approval vs Direct) based on published schema.
21
+ */
22
+ load(request) {
23
+ return this.http.post(`${this.baseUrl}/load`, request);
24
+ }
25
+ /**
26
+ * Submit form values. Result depends on mode:
27
+ * - Approval → status: 'PendingApproval'
28
+ * - Direct → status: 'Executed'
29
+ */
30
+ submit(request) {
31
+ return this.http.post(`${this.baseUrl}/submit`, request);
32
+ }
33
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
34
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormApiService, providedIn: 'root' });
35
+ }
36
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormApiService, decorators: [{
37
+ type: Injectable,
38
+ args: [{ providedIn: 'root' }]
39
+ }] });
40
+
41
+ /**
42
+ * Per-instance signal-based state for ClientForm.
43
+ *
44
+ * NOT providedIn root — each ClientForm component provides its own instance
45
+ * via `providers: [ClientFormStateService]`, enabling multiple independent
46
+ * forms on the same page.
47
+ */
48
+ class ClientFormStateService {
49
+ // ============================================================================
50
+ // Core State Signals
51
+ // ============================================================================
52
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
53
+ submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
54
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
55
+ submitError = signal(null, ...(ngDevMode ? [{ debugName: "submitError" }] : []));
56
+ loadResponse = signal(null, ...(ngDevMode ? [{ debugName: "loadResponse" }] : []));
57
+ submitResponse = signal(null, ...(ngDevMode ? [{ debugName: "submitResponse" }] : []));
58
+ // ============================================================================
59
+ // Derived Computeds — Load Response
60
+ // ============================================================================
61
+ isLoaded = computed(() => !!this.loadResponse(), ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
62
+ mode = computed(() => this.loadResponse()?.mode ?? null, ...(ngDevMode ? [{ debugName: "mode" }] : []));
63
+ isApproval = computed(() => this.mode() === 'Approval', ...(ngDevMode ? [{ debugName: "isApproval" }] : []));
64
+ isDirect = computed(() => this.mode() === 'Direct', ...(ngDevMode ? [{ debugName: "isDirect" }] : []));
65
+ formSource = computed(() => this.loadResponse()?.formSource ?? null, ...(ngDevMode ? [{ debugName: "formSource" }] : []));
66
+ isFallbackForm = computed(() => {
67
+ const source = this.formSource();
68
+ return source === 'ModuleFallback' || source === 'LevelFallback';
69
+ }, ...(ngDevMode ? [{ debugName: "isFallbackForm" }] : []));
70
+ requiresForm = computed(() => this.loadResponse()?.requiresForm ?? false, ...(ngDevMode ? [{ debugName: "requiresForm" }] : []));
71
+ formConfiguration = computed(() => this.loadResponse()?.formConfiguration ?? null, ...(ngDevMode ? [{ debugName: "formConfiguration" }] : []));
72
+ values = computed(() => this.loadResponse()?.values ?? [], ...(ngDevMode ? [{ debugName: "values" }] : []));
73
+ context = computed(() => this.loadResponse()?.context ?? null, ...(ngDevMode ? [{ debugName: "context" }] : []));
74
+ stepName = computed(() => this.loadResponse()?.stepName ?? null, ...(ngDevMode ? [{ debugName: "stepName" }] : []));
75
+ requestSchemaId = computed(() => this.loadResponse()?.requestSchemaId ?? null, ...(ngDevMode ? [{ debugName: "requestSchemaId" }] : []));
76
+ requestId = computed(() => this.loadResponse()?.requestId ?? null, ...(ngDevMode ? [{ debugName: "requestId" }] : []));
77
+ stepId = computed(() => this.loadResponse()?.stepId ?? null, ...(ngDevMode ? [{ debugName: "stepId" }] : []));
78
+ stepSchemaId = computed(() => this.loadResponse()?.stepSchemaId ?? null, ...(ngDevMode ? [{ debugName: "stepSchemaId" }] : []));
79
+ // ============================================================================
80
+ // Derived Computeds — Value Categories
81
+ // ============================================================================
82
+ /** Process virtual fields (Request_Date, Step_Name, etc.) — read-only display */
83
+ virtualFields = computed(() => this.values().filter((v) => v.metadata?.source === 'ProcessVirtual'), ...(ngDevMode ? [{ debugName: "virtualFields" }] : []));
84
+ /** Editable form values (non-virtual) */
85
+ formValues = computed(() => this.values().filter((v) => v.metadata?.source !== 'ProcessVirtual'), ...(ngDevMode ? [{ debugName: "formValues" }] : []));
86
+ // ============================================================================
87
+ // Derived Computeds — Submit Response
88
+ // ============================================================================
89
+ isSubmitted = computed(() => !!this.submitResponse(), ...(ngDevMode ? [{ debugName: "isSubmitted" }] : []));
90
+ submitStatus = computed(() => this.submitResponse()?.status ?? null, ...(ngDevMode ? [{ debugName: "submitStatus" }] : []));
91
+ isPendingApproval = computed(() => this.submitStatus() === 'PendingApproval', ...(ngDevMode ? [{ debugName: "isPendingApproval" }] : []));
92
+ isExecuted = computed(() => this.submitStatus() === 'Executed', ...(ngDevMode ? [{ debugName: "isExecuted" }] : []));
93
+ createdEntityId = computed(() => this.submitResponse()?.createdEntityId ?? null, ...(ngDevMode ? [{ debugName: "createdEntityId" }] : []));
94
+ // ============================================================================
95
+ // State Mutations
96
+ // ============================================================================
97
+ setLoadResponse(response) {
98
+ this.loadResponse.set(response);
99
+ this.error.set(null);
100
+ }
101
+ setSubmitResponse(response) {
102
+ this.submitResponse.set(response);
103
+ this.submitError.set(null);
104
+ }
105
+ setError(message) {
106
+ this.error.set(message);
107
+ this.loading.set(false);
108
+ }
109
+ setSubmitError(message) {
110
+ this.submitError.set(message);
111
+ this.submitting.set(false);
112
+ }
113
+ reset() {
114
+ this.loading.set(false);
115
+ this.submitting.set(false);
116
+ this.error.set(null);
117
+ this.submitError.set(null);
118
+ this.loadResponse.set(null);
119
+ this.submitResponse.set(null);
120
+ }
121
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
122
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormStateService });
123
+ }
124
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormStateService, decorators: [{
125
+ type: Injectable
126
+ }] });
127
+
128
+ // ============================================================================
129
+ // Constants
130
+ // ============================================================================
131
+ const WIDTH_TO_COLSPAN = {
132
+ '25': 3,
133
+ '50': 6,
134
+ '100': 12,
135
+ };
136
+ // ============================================================================
137
+ // Public Mapper Functions
138
+ // ============================================================================
139
+ /**
140
+ * Convert a runtime FormConfiguration into a DynamicFormConfig
141
+ * that can be passed directly to `<mt-dynamic-form>`.
142
+ *
143
+ * @param config The form configuration from the load API
144
+ * @param lang Current UI language ('en' | 'ar')
145
+ * @param mode 'create' or 'edit' — filters hidden fields accordingly
146
+ * @param lookups Available lookup definitions for resolving Lookup/LookupMultiSelect options
147
+ */
148
+ function mapToDynamicFormConfig(config, lang = 'en', mode = 'create', lookups = []) {
149
+ return {
150
+ sections: config.sections
151
+ .slice()
152
+ .sort((a, b) => a.order - b.order)
153
+ .map((section) => {
154
+ const sectionName = section.name[lang] ?? section.name['en'] ?? '';
155
+ const visibleFields = section.fields
156
+ .filter((field) => {
157
+ // isRead=false → completely hidden
158
+ if (field.isRead === false)
159
+ return false;
160
+ if (mode === 'create')
161
+ return !field.hiddenInCreation;
162
+ return !field.hiddenInEditForm;
163
+ })
164
+ .sort((a, b) => a.order - b.order);
165
+ return {
166
+ key: section.id,
167
+ label: sectionName,
168
+ type: 'header',
169
+ columns: 12,
170
+ order: section.order,
171
+ fields: visibleFields.map((field) => mapFieldToConfig(field, lang, lookups)),
172
+ };
173
+ })
174
+ .filter((section) => section.fields.length > 0),
175
+ };
176
+ }
177
+ /**
178
+ * Convert API property values into a flat key-value object
179
+ * suitable for `formControl.patchValue()`.
180
+ *
181
+ * Only includes non-virtual (editable) values.
182
+ */
183
+ function mapValuesToFormValue(values) {
184
+ const result = {};
185
+ for (const v of values) {
186
+ if (v.metadata?.source === 'ProcessVirtual')
187
+ continue;
188
+ result[v.propertyKey] = v.value;
189
+ }
190
+ return result;
191
+ }
192
+ /**
193
+ * Convert the current form value back into the submit payload format.
194
+ *
195
+ * Maps `requestPropertyId` from the load response metadata where available,
196
+ * so backend can match to schema request properties.
197
+ */
198
+ function mapFormValueToSubmitValues(formValue, loadResponse) {
199
+ const metadataByKey = new Map();
200
+ for (const v of loadResponse.values) {
201
+ if (v.metadata) {
202
+ metadataByKey.set(v.propertyKey, {
203
+ propertyId: v.metadata.propertyId,
204
+ });
205
+ }
206
+ }
207
+ return Object.entries(formValue)
208
+ .filter(([, value]) => value !== undefined && value !== null)
209
+ .map(([propertyKey, value]) => {
210
+ const meta = metadataByKey.get(propertyKey);
211
+ const submitValue = { propertyKey, value };
212
+ if (meta?.propertyId) {
213
+ submitValue.requestPropertyId = meta.propertyId;
214
+ }
215
+ return submitValue;
216
+ });
217
+ }
218
+ // ============================================================================
219
+ // Internal Helpers
220
+ // ============================================================================
221
+ /**
222
+ * Resolve the property item from either `property` or `propertyMetadata`.
223
+ * The API may return the data under either key.
224
+ */
225
+ function resolveProperty(field) {
226
+ return field.property ?? field.propertyMetadata;
227
+ }
228
+ function mapFieldToConfig(field, lang, lookups) {
229
+ const prop = resolveProperty(field);
230
+ const viewType = prop?.viewType ?? 'Text';
231
+ const label = resolvePropertyName(prop, lang) || field.propertyKey;
232
+ const colSpan = WIDTH_TO_COLSPAN[field.width] ?? 12;
233
+ const base = {
234
+ key: field.propertyKey,
235
+ label,
236
+ colSpan,
237
+ order: field.order,
238
+ placeholder: label,
239
+ required: field.isRequired ?? false,
240
+ readonly: field.isWrite === false,
241
+ validators: field.isRequired
242
+ ? [ValidatorConfig.required(`${label} is required`)]
243
+ : [],
244
+ };
245
+ switch (viewType) {
246
+ // ── Text-like ──────────────────────────────────────────────
247
+ case 'Text':
248
+ case 'Currency':
249
+ case 'EditableListView':
250
+ case 'LookupLog':
251
+ return new TextFieldConfig(base);
252
+ case 'LongText':
253
+ return new EditorFieldConfig(base);
254
+ // ── Numeric ───────────────────────────────────────────────
255
+ case 'Number':
256
+ return new NumberFieldConfig(base);
257
+ case 'Percentage':
258
+ return new SliderFieldConfig({ ...base, min: 0, max: 100 });
259
+ // ── Date / Time ───────────────────────────────────────────
260
+ case 'Date':
261
+ return new DateFieldConfig({ ...base, showTime: false });
262
+ case 'DateTime':
263
+ return new DateFieldConfig({ ...base, showTime: true });
264
+ case 'Time':
265
+ return new DateFieldConfig({ ...base, showTime: true });
266
+ // ── Boolean ───────────────────────────────────────────────
267
+ case 'Checkbox':
268
+ return new ToggleFieldConfig(base);
269
+ // ── File ──────────────────────────────────────────────────
270
+ case 'Attachment':
271
+ return new UploadFileFieldConfig(base);
272
+ // ── User Search ───────────────────────────────────────────
273
+ case 'User':
274
+ return new UserSearchFieldConfig({
275
+ ...base,
276
+ apiUrl: 'Identity/users',
277
+ context: new HttpContext().set(REQUEST_CONTEXT, {
278
+ useBaseUrl: false,
279
+ }),
280
+ });
281
+ // ── Lookup (single select) ────────────────────────────────
282
+ case 'Lookup': {
283
+ const items = resolveLookupOptions(prop, lookups);
284
+ return new SelectFieldConfig({
285
+ ...base,
286
+ options: items,
287
+ optionLabel: 'label',
288
+ optionValue: 'value',
289
+ filter: items.length > 10,
290
+ showClear: !(field.isRequired ?? false),
291
+ });
292
+ }
293
+ // ── Lookup (multi select) ─────────────────────────────────
294
+ case 'LookupMultiSelect': {
295
+ const items = resolveLookupOptions(prop, lookups);
296
+ return new MultiSelectFieldConfig({
297
+ ...base,
298
+ options: items,
299
+ optionLabel: 'label',
300
+ optionValue: 'value',
301
+ filter: items.length > 10,
302
+ display: 'chip',
303
+ });
304
+ }
305
+ // ── Other select-based types ──────────────────────────────
306
+ case 'Status':
307
+ case 'InternalModule':
308
+ case 'DynamicList':
309
+ case 'API':
310
+ case 'LookupMatrix':
311
+ case 'Location': {
312
+ const options = extractOptionsFromProperty(prop);
313
+ return new SelectFieldConfig({
314
+ ...base,
315
+ options: options ?? [],
316
+ optionLabel: 'label',
317
+ optionValue: 'value',
318
+ });
319
+ }
320
+ // ── Fallback ──────────────────────────────────────────────
321
+ default:
322
+ return new TextFieldConfig(base);
323
+ }
324
+ }
325
+ function resolvePropertyName(property, lang) {
326
+ if (!property?.name)
327
+ return '';
328
+ if (typeof property.name === 'string')
329
+ return property.name;
330
+ // Prefer display name, then lang-specific, then English fallback
331
+ return property.name['display'] ?? property.name[lang] ?? property.name['en'] ?? '';
332
+ }
333
+ /**
334
+ * Resolve lookup items for Lookup / LookupMultiSelect viewTypes.
335
+ *
336
+ * Reads `configuration.lookup` (the lookup ID) from the property metadata,
337
+ * finds the matching lookup definition, and maps its items to select options.
338
+ */
339
+ function resolveLookupOptions(prop, lookups) {
340
+ const lookupId = prop?.configuration?.['lookup'];
341
+ if (!lookupId || !lookups.length)
342
+ return [];
343
+ const lookup = lookups.find((l) => l.id === lookupId);
344
+ if (!lookup)
345
+ return [];
346
+ return lookup.items
347
+ .slice()
348
+ .sort((a, b) => a.order - b.order)
349
+ .map((item) => ({
350
+ label: item.name?.display ?? item.key,
351
+ value: item.id,
352
+ }));
353
+ }
354
+ /**
355
+ * Fallback option extractor for non-lookup select types
356
+ * (Status, InternalModule, DynamicList, API, etc.).
357
+ */
358
+ function extractOptionsFromProperty(property) {
359
+ if (!property?.configuration)
360
+ return null;
361
+ const config = property.configuration;
362
+ if (Array.isArray(config['options'])) {
363
+ return config['options'];
364
+ }
365
+ if (Array.isArray(config['items'])) {
366
+ return config['items'].map((item) => ({
367
+ label: typeof item.name === 'string'
368
+ ? item.name
369
+ : (item.name?.['en'] ?? item.label ?? String(item.value)),
370
+ value: item.id ?? item.value ?? item.key,
371
+ }));
372
+ }
373
+ return null;
374
+ }
375
+
376
+ /**
377
+ * Client Form — Runtime process form component.
378
+ *
379
+ * Self-contained, signal-based (no NGXS). Each instance manages its own state
380
+ * via a component-scoped `ClientFormStateService`.
381
+ *
382
+ * **No action buttons in template.** Parent controls all actions via `viewChild()`:
383
+ *
384
+ * ```html
385
+ * <mt-client-form #processForm [moduleKey]="'Risk'" [operationKey]="'CloseRisk'" />
386
+ * <button (click)="processForm.load()">Load</button>
387
+ * <button (click)="processForm.submit()">Submit</button>
388
+ * ```
389
+ *
390
+ * Or programmatically:
391
+ * ```typescript
392
+ * readonly processForm = viewChild.required(ClientForm);
393
+ * this.processForm().load();
394
+ * this.processForm().submit();
395
+ * ```
396
+ */
397
+ class ClientForm {
398
+ api = inject(ClientFormApiService);
399
+ state = inject(ClientFormStateService);
400
+ loadSub;
401
+ submitSub;
402
+ // ============================================================================
403
+ // Public State Signals (for parent access via viewChild)
404
+ // ============================================================================
405
+ submitting = computed(() => this.state.submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
406
+ submitError = computed(() => this.state.submitError(), ...(ngDevMode ? [{ debugName: "submitError" }] : []));
407
+ isSubmitted = computed(() => this.state.isSubmitted(), ...(ngDevMode ? [{ debugName: "isSubmitted" }] : []));
408
+ isPendingApproval = computed(() => this.state.isPendingApproval(), ...(ngDevMode ? [{ debugName: "isPendingApproval" }] : []));
409
+ isExecuted = computed(() => this.state.isExecuted(), ...(ngDevMode ? [{ debugName: "isExecuted" }] : []));
410
+ isLoaded = computed(() => this.state.isLoaded(), ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
411
+ loading = computed(() => this.state.loading(), ...(ngDevMode ? [{ debugName: "loading" }] : []));
412
+ // ============================================================================
413
+ // Inputs — Required Context
414
+ // ============================================================================
415
+ moduleKey = input.required(...(ngDevMode ? [{ debugName: "moduleKey" }] : []));
416
+ operationKey = input.required(...(ngDevMode ? [{ debugName: "operationKey" }] : []));
417
+ // ============================================================================
418
+ // Inputs — Optional Context
419
+ // ============================================================================
420
+ moduleId = input(...(ngDevMode ? [undefined, { debugName: "moduleId" }] : []));
421
+ levelId = input(...(ngDevMode ? [undefined, { debugName: "levelId" }] : []));
422
+ levelDataId = input(...(ngDevMode ? [undefined, { debugName: "levelDataId" }] : []));
423
+ moduleDataId = input(...(ngDevMode ? [undefined, { debugName: "moduleDataId" }] : []));
424
+ requestSchemaId = input(...(ngDevMode ? [undefined, { debugName: "requestSchemaId" }] : []));
425
+ draftProcessId = input(...(ngDevMode ? [undefined, { debugName: "draftProcessId" }] : []));
426
+ preview = input(false, ...(ngDevMode ? [{ debugName: "preview" }] : []));
427
+ returnUrl = input(...(ngDevMode ? [undefined, { debugName: "returnUrl" }] : []));
428
+ // ============================================================================
429
+ // Inputs — UI Configuration
430
+ // ============================================================================
431
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
432
+ autoLoad = input(true, ...(ngDevMode ? [{ debugName: "autoLoad" }] : []));
433
+ formMode = input('create', ...(ngDevMode ? [{ debugName: "formMode" }] : []));
434
+ lang = input('en', ...(ngDevMode ? [{ debugName: "lang" }] : []));
435
+ lookups = input([], ...(ngDevMode ? [{ debugName: "lookups" }] : []));
436
+ // ============================================================================
437
+ // Outputs
438
+ // ============================================================================
439
+ loaded = output();
440
+ submitted = output();
441
+ errored = output();
442
+ modeDetected = output();
443
+ formSourceDetected = output();
444
+ // ============================================================================
445
+ // Internal Form Control
446
+ // ============================================================================
447
+ formControl = new FormControl({});
448
+ // ============================================================================
449
+ // Computed — Dynamic Form Config
450
+ // ============================================================================
451
+ formConfig = computed(() => {
452
+ const config = this.state.formConfiguration();
453
+ if (!config)
454
+ return null;
455
+ return mapToDynamicFormConfig(config, this.lang(), this.formMode(), this.lookups());
456
+ }, ...(ngDevMode ? [{ debugName: "formConfig" }] : []));
457
+ initialValues = computed(() => {
458
+ return mapValuesToFormValue(this.state.formValues());
459
+ }, ...(ngDevMode ? [{ debugName: "initialValues" }] : []));
460
+ virtualFields = computed(() => this.state.virtualFields(), ...(ngDevMode ? [{ debugName: "virtualFields" }] : []));
461
+ hasVirtualFields = computed(() => this.virtualFields().length > 0, ...(ngDevMode ? [{ debugName: "hasVirtualFields" }] : []));
462
+ // ============================================================================
463
+ // Effects
464
+ // ============================================================================
465
+ constructor() {
466
+ // Auto-load when inputs are ready
467
+ effect(() => {
468
+ const autoLoad = this.autoLoad();
469
+ const moduleKey = this.moduleKey();
470
+ const operationKey = this.operationKey();
471
+ if (autoLoad && moduleKey && operationKey) {
472
+ untracked(() => this.load());
473
+ }
474
+ });
475
+ // Patch form values after load
476
+ effect(() => {
477
+ const values = this.initialValues();
478
+ const isLoaded = this.state.isLoaded();
479
+ if (isLoaded && Object.keys(values).length > 0) {
480
+ untracked(() => {
481
+ this.formControl.patchValue(values, { emitEvent: false });
482
+ });
483
+ }
484
+ });
485
+ }
486
+ // ============================================================================
487
+ // Public API (accessed via viewChild)
488
+ // ============================================================================
489
+ /**
490
+ * Load form configuration from the API.
491
+ * Builds request from current input values.
492
+ */
493
+ load() {
494
+ if (this.state.loading())
495
+ return;
496
+ this.loadSub?.unsubscribe();
497
+ this.state.loading.set(true);
498
+ this.state.error.set(null);
499
+ this.state.submitResponse.set(null);
500
+ const request = this.buildLoadRequest();
501
+ this.loadSub = this.api.load(request).subscribe({
502
+ next: (response) => {
503
+ this.state.loading.set(false);
504
+ if (response.data) {
505
+ this.state.setLoadResponse(response.data);
506
+ this.loaded.emit(response.data);
507
+ if (response.data.mode) {
508
+ this.modeDetected.emit(response.data.mode);
509
+ }
510
+ if (response.data.formSource) {
511
+ this.formSourceDetected.emit(response.data.formSource);
512
+ }
513
+ }
514
+ else {
515
+ const msg = response.message ?? 'Failed to load form';
516
+ this.state.setError(msg);
517
+ this.errored.emit(msg);
518
+ }
519
+ },
520
+ error: (err) => {
521
+ const msg = err?.error?.message ?? err?.message ?? 'Failed to load form';
522
+ this.state.setError(msg);
523
+ this.errored.emit(msg);
524
+ },
525
+ });
526
+ }
527
+ /**
528
+ * Submit the current form values.
529
+ * Builds submit request from form value + load context.
530
+ */
531
+ submit() {
532
+ if (this.state.submitting())
533
+ return;
534
+ this.submitSub?.unsubscribe();
535
+ this.state.submitting.set(true);
536
+ this.state.submitError.set(null);
537
+ const request = this.buildSubmitRequest();
538
+ this.submitSub = this.api.submit(request).subscribe({
539
+ next: (response) => {
540
+ this.state.submitting.set(false);
541
+ if (response.data) {
542
+ this.state.setSubmitResponse(response.data);
543
+ this.submitted.emit(response.data);
544
+ }
545
+ else {
546
+ const msg = response.message ?? 'Failed to submit form';
547
+ this.state.setSubmitError(msg);
548
+ this.errored.emit(msg);
549
+ }
550
+ },
551
+ error: (err) => {
552
+ const msg = err?.error?.message ?? err?.message ?? 'Failed to submit form';
553
+ this.state.setSubmitError(msg);
554
+ this.errored.emit(msg);
555
+ },
556
+ });
557
+ }
558
+ /**
559
+ * Get the current form value as a flat key-value object.
560
+ */
561
+ getFormValue() {
562
+ return this.formControl.value ?? {};
563
+ }
564
+ /**
565
+ * Get the current form value mapped to submit payload format.
566
+ */
567
+ getSubmitValues() {
568
+ const loadResponse = this.state.loadResponse();
569
+ if (!loadResponse)
570
+ return [];
571
+ return mapFormValueToSubmitValues(this.getFormValue(), loadResponse);
572
+ }
573
+ /**
574
+ * Check whether the current form state is valid.
575
+ */
576
+ isValid() {
577
+ return this.formControl.valid;
578
+ }
579
+ /**
580
+ * Reset the component to its initial state.
581
+ */
582
+ reset() {
583
+ this.loadSub?.unsubscribe();
584
+ this.submitSub?.unsubscribe();
585
+ this.formControl.reset({});
586
+ this.state.reset();
587
+ }
588
+ // ============================================================================
589
+ // Lifecycle
590
+ // ============================================================================
591
+ ngOnDestroy() {
592
+ this.loadSub?.unsubscribe();
593
+ this.submitSub?.unsubscribe();
594
+ }
595
+ // ============================================================================
596
+ // Private Helpers
597
+ // ============================================================================
598
+ buildLoadRequest() {
599
+ const req = {
600
+ moduleKey: this.moduleKey(),
601
+ operationKey: this.operationKey(),
602
+ };
603
+ const moduleId = this.moduleId();
604
+ const levelId = this.levelId();
605
+ const levelDataId = this.levelDataId();
606
+ const moduleDataId = this.moduleDataId();
607
+ const requestSchemaId = this.requestSchemaId();
608
+ const draftProcessId = this.draftProcessId();
609
+ const preview = this.preview();
610
+ if (moduleId != null)
611
+ req.moduleId = moduleId;
612
+ if (levelId != null)
613
+ req.levelId = levelId;
614
+ if (levelDataId != null)
615
+ req.levelDataId = levelDataId;
616
+ if (moduleDataId != null)
617
+ req.moduleDataId = moduleDataId;
618
+ if (requestSchemaId != null)
619
+ req.requestSchemaId = requestSchemaId;
620
+ if (draftProcessId != null)
621
+ req.draftProcessId = draftProcessId;
622
+ if (preview)
623
+ req.preview = preview;
624
+ return req;
625
+ }
626
+ buildSubmitRequest() {
627
+ const loadResponse = this.state.loadResponse();
628
+ const context = this.state.context();
629
+ const formValue = this.getFormValue();
630
+ const values = loadResponse
631
+ ? mapFormValueToSubmitValues(formValue, loadResponse)
632
+ : Object.entries(formValue)
633
+ .filter(([, v]) => v !== undefined && v !== null)
634
+ .map(([propertyKey, value]) => ({ propertyKey, value }));
635
+ const req = {
636
+ moduleKey: context?.moduleKey ?? this.moduleKey(),
637
+ operationKey: context?.operationKey ?? this.operationKey(),
638
+ values,
639
+ };
640
+ const moduleId = context?.moduleId ?? this.moduleId();
641
+ const levelId = context?.levelId ?? this.levelId();
642
+ const levelDataId = context?.levelDataId ?? this.levelDataId();
643
+ const moduleDataId = context?.moduleDataId ?? this.moduleDataId();
644
+ const requestSchemaId = context?.requestSchemaId ?? this.requestSchemaId();
645
+ const draftProcessId = this.draftProcessId();
646
+ const returnUrl = this.returnUrl();
647
+ if (moduleId != null)
648
+ req.moduleId = moduleId;
649
+ if (levelId != null)
650
+ req.levelId = levelId;
651
+ if (levelDataId != null)
652
+ req.levelDataId = levelDataId;
653
+ if (moduleDataId != null)
654
+ req.moduleDataId = moduleDataId;
655
+ if (requestSchemaId != null)
656
+ req.requestSchemaId = requestSchemaId;
657
+ if (draftProcessId != null)
658
+ req.draftProcessId = draftProcessId;
659
+ if (returnUrl)
660
+ req.returnUrl = returnUrl;
661
+ return req;
662
+ }
663
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
664
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: ClientForm, isStandalone: true, selector: "mt-client-form", inputs: { moduleKey: { classPropertyName: "moduleKey", publicName: "moduleKey", isSignal: true, isRequired: true, transformFunction: null }, operationKey: { classPropertyName: "operationKey", publicName: "operationKey", isSignal: true, isRequired: true, transformFunction: null }, moduleId: { classPropertyName: "moduleId", publicName: "moduleId", isSignal: true, isRequired: false, transformFunction: null }, levelId: { classPropertyName: "levelId", publicName: "levelId", isSignal: true, isRequired: false, transformFunction: null }, levelDataId: { classPropertyName: "levelDataId", publicName: "levelDataId", isSignal: true, isRequired: false, transformFunction: null }, moduleDataId: { classPropertyName: "moduleDataId", publicName: "moduleDataId", isSignal: true, isRequired: false, transformFunction: null }, requestSchemaId: { classPropertyName: "requestSchemaId", publicName: "requestSchemaId", isSignal: true, isRequired: false, transformFunction: null }, draftProcessId: { classPropertyName: "draftProcessId", publicName: "draftProcessId", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, returnUrl: { classPropertyName: "returnUrl", publicName: "returnUrl", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, autoLoad: { classPropertyName: "autoLoad", publicName: "autoLoad", isSignal: true, isRequired: false, transformFunction: null }, formMode: { classPropertyName: "formMode", publicName: "formMode", isSignal: true, isRequired: false, transformFunction: null }, lang: { classPropertyName: "lang", publicName: "lang", isSignal: true, isRequired: false, transformFunction: null }, lookups: { classPropertyName: "lookups", publicName: "lookups", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", submitted: "submitted", errored: "errored", modeDetected: "modeDetected", formSourceDetected: "formSourceDetected" }, providers: [ClientFormStateService], ngImport: i0, template: "<!-- Client Form Template \u2014 Render only, NO action buttons -->\r\n\r\n<!-- Loading State -->\r\n@if (state.loading()) {\r\n <div class=\"flex flex-col gap-4 animate-pulse\">\r\n <div class=\"h-6 bg-surface-200 rounded w-1/3\"></div>\r\n <div class=\"grid grid-cols-12 gap-4\">\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-12 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- Error State -->\r\n@if (state.error(); as error) {\r\n <div\r\n class=\"flex items-center gap-2 p-3 rounded-lg bg-red-50 text-red-700 border border-red-200\"\r\n role=\"alert\"\r\n >\r\n <svg class=\"w-5 h-5 shrink-0\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\r\n <path\r\n fill-rule=\"evenodd\"\r\n d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z\"\r\n clip-rule=\"evenodd\"\r\n />\r\n </svg>\r\n <span class=\"text-sm font-medium\">{{ error }}</span>\r\n </div>\r\n}\r\n\r\n<!-- Loaded State -->\r\n@if (state.isLoaded() && !state.loading()) {\r\n <!-- Step Info Bar -->\r\n @if (state.stepName() || state.mode()) {\r\n <div\r\n class=\"flex items-center gap-3 mb-4 p-3 rounded-lg bg-surface-50 border border-surface-200\"\r\n >\r\n @if (state.stepName()) {\r\n <span class=\"text-sm font-semibold text-surface-700\">\r\n {{ state.stepName() }}\r\n </span>\r\n }\r\n\r\n @if (state.mode()) {\r\n <span\r\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\r\n [class]=\"\r\n state.isApproval()\r\n ? 'bg-amber-100 text-amber-800'\r\n : 'bg-emerald-100 text-emerald-800'\r\n \"\r\n >\r\n {{ state.mode() }}\r\n </span>\r\n }\r\n\r\n @if (state.isFallbackForm()) {\r\n <span\r\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-800\"\r\n >\r\n {{ state.formSource() }}\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Virtual Fields (read-only process context) -->\r\n @if (hasVirtualFields()) {\r\n <div class=\"mb-4 p-3 rounded-lg bg-surface-50 border border-surface-200\">\r\n <div\r\n class=\"grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3 lg:grid-cols-4\"\r\n >\r\n @for (field of virtualFields(); track field.propertyKey) {\r\n <div class=\"flex flex-col gap-0.5\">\r\n <span class=\"text-xs text-muted-color font-medium\">\r\n {{ field.propertyKey | titlecase }}\r\n </span>\r\n <span class=\"text-sm text-surface-800 font-medium\">\r\n {{ field.value ?? \"\u2014\" }}\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Dynamic Form -->\r\n @if (state.requiresForm() && formConfig(); as config) {\r\n <mt-dynamic-form [formConfig]=\"config\" [formControl]=\"formControl\" />\r\n } @else if (!state.requiresForm()) {\r\n <div\r\n class=\"flex items-center justify-center p-6 rounded-lg bg-surface-50 border border-surface-200 border-dashed\"\r\n >\r\n <p class=\"text-sm text-muted-color\">\r\n No form required for this operation.\r\n </p>\r\n </div>\r\n }\r\n}\r\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig"] }, { kind: "pipe", type: i2.TitleCasePipe, name: "titlecase" }] });
665
+ }
666
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientForm, decorators: [{
667
+ type: Component,
668
+ args: [{ selector: 'mt-client-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicForm], providers: [ClientFormStateService], template: "<!-- Client Form Template \u2014 Render only, NO action buttons -->\r\n\r\n<!-- Loading State -->\r\n@if (state.loading()) {\r\n <div class=\"flex flex-col gap-4 animate-pulse\">\r\n <div class=\"h-6 bg-surface-200 rounded w-1/3\"></div>\r\n <div class=\"grid grid-cols-12 gap-4\">\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-12 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"col-span-6 h-10 bg-surface-200 rounded\"></div>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- Error State -->\r\n@if (state.error(); as error) {\r\n <div\r\n class=\"flex items-center gap-2 p-3 rounded-lg bg-red-50 text-red-700 border border-red-200\"\r\n role=\"alert\"\r\n >\r\n <svg class=\"w-5 h-5 shrink-0\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\r\n <path\r\n fill-rule=\"evenodd\"\r\n d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z\"\r\n clip-rule=\"evenodd\"\r\n />\r\n </svg>\r\n <span class=\"text-sm font-medium\">{{ error }}</span>\r\n </div>\r\n}\r\n\r\n<!-- Loaded State -->\r\n@if (state.isLoaded() && !state.loading()) {\r\n <!-- Step Info Bar -->\r\n @if (state.stepName() || state.mode()) {\r\n <div\r\n class=\"flex items-center gap-3 mb-4 p-3 rounded-lg bg-surface-50 border border-surface-200\"\r\n >\r\n @if (state.stepName()) {\r\n <span class=\"text-sm font-semibold text-surface-700\">\r\n {{ state.stepName() }}\r\n </span>\r\n }\r\n\r\n @if (state.mode()) {\r\n <span\r\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\r\n [class]=\"\r\n state.isApproval()\r\n ? 'bg-amber-100 text-amber-800'\r\n : 'bg-emerald-100 text-emerald-800'\r\n \"\r\n >\r\n {{ state.mode() }}\r\n </span>\r\n }\r\n\r\n @if (state.isFallbackForm()) {\r\n <span\r\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-800\"\r\n >\r\n {{ state.formSource() }}\r\n </span>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Virtual Fields (read-only process context) -->\r\n @if (hasVirtualFields()) {\r\n <div class=\"mb-4 p-3 rounded-lg bg-surface-50 border border-surface-200\">\r\n <div\r\n class=\"grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3 lg:grid-cols-4\"\r\n >\r\n @for (field of virtualFields(); track field.propertyKey) {\r\n <div class=\"flex flex-col gap-0.5\">\r\n <span class=\"text-xs text-muted-color font-medium\">\r\n {{ field.propertyKey | titlecase }}\r\n </span>\r\n <span class=\"text-sm text-surface-800 font-medium\">\r\n {{ field.value ?? \"\u2014\" }}\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Dynamic Form -->\r\n @if (state.requiresForm() && formConfig(); as config) {\r\n <mt-dynamic-form [formConfig]=\"config\" [formControl]=\"formControl\" />\r\n } @else if (!state.requiresForm()) {\r\n <div\r\n class=\"flex items-center justify-center p-6 rounded-lg bg-surface-50 border border-surface-200 border-dashed\"\r\n >\r\n <p class=\"text-sm text-muted-color\">\r\n No form required for this operation.\r\n </p>\r\n </div>\r\n }\r\n}\r\n", styles: [":host{display:block}\n"] }]
669
+ }], ctorParameters: () => [], propDecorators: { moduleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "moduleKey", required: true }] }], operationKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "operationKey", required: true }] }], moduleId: [{ type: i0.Input, args: [{ isSignal: true, alias: "moduleId", required: false }] }], levelId: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelId", required: false }] }], levelDataId: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelDataId", required: false }] }], moduleDataId: [{ type: i0.Input, args: [{ isSignal: true, alias: "moduleDataId", required: false }] }], requestSchemaId: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestSchemaId", required: false }] }], draftProcessId: [{ type: i0.Input, args: [{ isSignal: true, alias: "draftProcessId", required: false }] }], preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], returnUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnUrl", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], autoLoad: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoLoad", required: false }] }], formMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "formMode", required: false }] }], lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "lang", required: false }] }], lookups: [{ type: i0.Input, args: [{ isSignal: true, alias: "lookups", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], errored: [{ type: i0.Output, args: ["errored"] }], modeDetected: [{ type: i0.Output, args: ["modeDetected"] }], formSourceDetected: [{ type: i0.Output, args: ["formSourceDetected"] }] } });
670
+
671
+ // ============================================================================
672
+ // API Response Wrapper
673
+ // ============================================================================
674
+ /**
675
+ * Type guard to detect a FormRequired interception response from legacy commands.
676
+ * Use in HTTP interceptors to redirect to process-forms flow.
677
+ */
678
+ function isFormRequiredInterception(response) {
679
+ return (response?.status === 'FormRequired' &&
680
+ typeof response?.requestSchemaId === 'number');
681
+ }
682
+
683
+ // Client Form - Runtime process form component
684
+
685
+ /**
686
+ * Generated bundle index. Do not edit.
687
+ */
688
+
689
+ export { ClientForm, ClientFormApiService, ClientFormStateService, isFormRequiredInterception, mapFormValueToSubmitValues, mapToDynamicFormConfig, mapValuesToFormValue };
690
+ //# sourceMappingURL=masterteam-forms-client-form.mjs.map