@codex-ts/core-lib 1.0.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.
Files changed (76) hide show
  1. package/dist/core-lib/fesm2022/core-lib.mjs +2349 -0
  2. package/dist/core-lib/fesm2022/core-lib.mjs.map +1 -0
  3. package/dist/core-lib/index.d.ts +29 -0
  4. package/dist/core-lib/lib/components/base-form.component.d.ts +62 -0
  5. package/dist/core-lib/lib/components/base-list-page.component.d.ts +132 -0
  6. package/dist/core-lib/lib/components/base-page.component.d.ts +25 -0
  7. package/dist/core-lib/lib/components/base-tabbed-form.component.d.ts +54 -0
  8. package/dist/core-lib/lib/components/formly/core-formly.module.d.ts +7 -0
  9. package/dist/core-lib/lib/components/formly/form-field.wrapper.d.ts +21 -0
  10. package/dist/core-lib/lib/components/formly/formly.constants.d.ts +13 -0
  11. package/dist/core-lib/lib/components/formly/types/checkbox.type.d.ts +12 -0
  12. package/dist/core-lib/lib/components/formly/types/date.type.d.ts +20 -0
  13. package/dist/core-lib/lib/components/formly/types/multiselect.type.d.ts +26 -0
  14. package/dist/core-lib/lib/components/formly/types/numeric-input.type.d.ts +17 -0
  15. package/dist/core-lib/lib/components/formly/types/radio.type.d.ts +13 -0
  16. package/dist/core-lib/lib/components/formly/types/select.type.d.ts +25 -0
  17. package/dist/core-lib/lib/components/formly/types/text-input.type.d.ts +12 -0
  18. package/dist/core-lib/lib/components/formly/types/textarea.type.d.ts +14 -0
  19. package/dist/core-lib/lib/constants/app.constants.d.ts +13 -0
  20. package/dist/core-lib/lib/constants/app.messages.d.ts +36 -0
  21. package/dist/core-lib/lib/enums/component-context.enum.d.ts +6 -0
  22. package/dist/core-lib/lib/enums/tabbed-form-type.enum.d.ts +4 -0
  23. package/dist/core-lib/lib/models/pagination/filter-criteria.interface.d.ts +9 -0
  24. package/dist/core-lib/lib/models/pagination/page-request.interface.d.ts +10 -0
  25. package/dist/core-lib/lib/models/pagination/page.interface.d.ts +9 -0
  26. package/dist/core-lib/lib/models/pagination/sort-criteria.interface.d.ts +4 -0
  27. package/dist/core-lib/lib/pipes/indian-date.pipe.d.ts +12 -0
  28. package/dist/core-lib/lib/services/base-crud.service.d.ts +36 -0
  29. package/dist/core-lib/lib/services/entity-registry.service.d.ts +26 -0
  30. package/dist/core-lib/lib/services/entity.service.d.ts +9 -0
  31. package/dist/core-lib/lib/services/enum-registry.service.d.ts +41 -0
  32. package/dist/core-lib/lib/services/formly-config.service.d.ts +11 -0
  33. package/dist/core-lib/lib/services/http-utility.service.d.ts +130 -0
  34. package/dist/core-lib/lib/services/notification.service.d.ts +13 -0
  35. package/dist/core-lib/lib/services/reference-data-provider.service.d.ts +8 -0
  36. package/dist/core-lib/lib/services/utils.service.d.ts +72 -0
  37. package/ng-package.json +7 -0
  38. package/package.json +63 -0
  39. package/src/index.ts +44 -0
  40. package/src/lib/components/base-form.component.ts +328 -0
  41. package/src/lib/components/base-list-page.component.ts +327 -0
  42. package/src/lib/components/base-page.component.ts +33 -0
  43. package/src/lib/components/base-tabbed-form.component.ts +345 -0
  44. package/src/lib/components/formly/core-formly.module.ts +34 -0
  45. package/src/lib/components/formly/field.interface.ts +15 -0
  46. package/src/lib/components/formly/form-field.wrapper.ts +94 -0
  47. package/src/lib/components/formly/formly.constants.ts +14 -0
  48. package/src/lib/components/formly/primeng-formly.module.ts +77 -0
  49. package/src/lib/components/formly/types/checkbox.type.ts +43 -0
  50. package/src/lib/components/formly/types/date.type.ts +88 -0
  51. package/src/lib/components/formly/types/multiselect.type.ts +61 -0
  52. package/src/lib/components/formly/types/numeric-input.type.ts +45 -0
  53. package/src/lib/components/formly/types/radio.type.ts +47 -0
  54. package/src/lib/components/formly/types/select.type.ts +54 -0
  55. package/src/lib/components/formly/types/text-input.type.ts +33 -0
  56. package/src/lib/components/formly/types/textarea.type.ts +37 -0
  57. package/src/lib/constants/app.constants.ts +15 -0
  58. package/src/lib/constants/app.messages.ts +36 -0
  59. package/src/lib/enums/component-context.enum.ts +6 -0
  60. package/src/lib/enums/tabbed-form-type.enum.ts +4 -0
  61. package/src/lib/models/pagination/filter-criteria.interface.ts +19 -0
  62. package/src/lib/models/pagination/page-request.interface.ts +13 -0
  63. package/src/lib/models/pagination/page.interface.ts +9 -0
  64. package/src/lib/models/pagination/sort-criteria.interface.ts +4 -0
  65. package/src/lib/pipes/indian-date.pipe.ts +46 -0
  66. package/src/lib/services/base-crud.service.ts +81 -0
  67. package/src/lib/services/entity-registry.service.ts +51 -0
  68. package/src/lib/services/entity.service.ts +10 -0
  69. package/src/lib/services/enum-registry.service.ts +66 -0
  70. package/src/lib/services/formly-config.service.ts +50 -0
  71. package/src/lib/services/http-utility.service.example.ts +253 -0
  72. package/src/lib/services/http-utility.service.md +175 -0
  73. package/src/lib/services/http-utility.service.ts +389 -0
  74. package/src/lib/services/notification.service.ts +35 -0
  75. package/src/lib/services/reference-data-provider.service.ts +28 -0
  76. package/src/lib/services/utils.service.ts +154 -0
@@ -0,0 +1,327 @@
1
+ import { OnInit, OnDestroy, ViewChild, Directive } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import { Table, TableLazyLoadEvent } from 'primeng/table';
4
+ import { ConfirmationService } from 'primeng/api';
5
+ import { NotificationService } from '../services/notification.service';
6
+ import { Subscription, Observable } from 'rxjs';
7
+ import { finalize } from 'rxjs/operators';
8
+ import { Page } from '../models/pagination/page.interface';
9
+ import { PageRequest } from '../models/pagination/page-request.interface';
10
+ import { FilterCriteria, FilterOperator } from '../models/pagination/filter-criteria.interface';
11
+ import { SortCriteria } from '../models/pagination/sort-criteria.interface';
12
+ import { BasePageComponent } from './base-page.component';
13
+ import { AppMessages } from '../constants/app.messages';
14
+ import { UtilsService } from '../services/utils.service';
15
+ import { AppConstants } from '../constants/app.constants';
16
+
17
+ /**
18
+ * Base component for all list page components in the application.
19
+ * Provides common functionality for list operations like loading data,
20
+ * pagination, sorting, and CRUD operations.
21
+ */
22
+ @Directive()
23
+ export abstract class BaseListPageComponent<T extends { uuid: string }>
24
+ extends BasePageComponent implements OnInit, OnDestroy {
25
+
26
+ @ViewChild('dt') table!: Table;
27
+ items: T[] = [];
28
+ actionLoading: boolean = false;
29
+ rows: number = 10;
30
+ totalRecords: number = 0;
31
+ filters: FilterCriteria[] = [];
32
+ protected subscriptions: Subscription[] = [];
33
+
34
+ constructor(
35
+ protected router: Router,
36
+ protected override notificationService: NotificationService,
37
+ protected override utilsService: UtilsService,
38
+ protected confirmationService: ConfirmationService
39
+ ) {
40
+ super(utilsService, notificationService);
41
+ }
42
+
43
+ /**
44
+ * Initialize the component, subscribe to loading state if available,
45
+ * and load initial data.
46
+ */
47
+ ngOnInit() {
48
+ // Subscribe to loading state if available
49
+ const loadingObservable = this.getLoadingObservable();
50
+ if (loadingObservable) {
51
+ this.subscriptions.push(
52
+ loadingObservable.subscribe(
53
+ isLoading => this.loading = isLoading
54
+ )
55
+ );
56
+ }
57
+ this.loadData();
58
+ }
59
+
60
+ /**
61
+ * Clean up any subscriptions when the component is destroyed
62
+ */
63
+ ngOnDestroy() {
64
+ this.subscriptions.forEach(sub => sub.unsubscribe());
65
+ }
66
+
67
+ /**
68
+ * Handle search event from the UI
69
+ * @param event The search event
70
+ */
71
+ onSearch(event: Event): void {
72
+ const element = event.target as HTMLInputElement;
73
+ if (this.table) {
74
+ this.table.filterGlobal(element.value, 'contains');
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Load data with pagination and sorting
80
+ * @param event The lazy load event from PrimeNG table
81
+ */
82
+ /**
83
+ * Convert PrimeNG filter format to backend format
84
+ * @param filters The PrimeNG filters object
85
+ * @returns Converted filters object for backend
86
+ */
87
+ /**
88
+ * Convert PrimeNG filter format to backend format
89
+ * @param filters The PrimeNG filters object
90
+ * @returns Converted filters object for backend
91
+ */
92
+ /**
93
+ * Convert PrimeNG filters to backend format with proper operator mapping
94
+ */
95
+ protected convertFilters(filters: { [key: string]: any }): FilterCriteria[] {
96
+ const backendFilters: FilterCriteria[] = [];
97
+ if (!filters) return backendFilters;
98
+
99
+ console.log('Raw PrimeNG filters:', filters);
100
+
101
+ Object.entries(filters).forEach(([key, filterMeta]) => {
102
+ console.log(`Processing filter for ${key}:`, filterMeta);
103
+
104
+ // Handle array of filter metadata (multiple filters for same column)
105
+ if (Array.isArray(filterMeta)) {
106
+ const validFilters = filterMeta
107
+ .filter(meta => meta.value !== null && meta.value !== undefined && meta.value !== '')
108
+ .map(meta => ({
109
+ field: key,
110
+ value: this.transformFilterValue(String(meta.value)),
111
+ operator: this.transformOperator(meta.matchMode)
112
+ }));
113
+ backendFilters.push(...validFilters);
114
+ }
115
+ // Handle single filter metadata
116
+ else if (filterMeta?.value !== null && filterMeta?.value !== undefined && filterMeta?.value !== '') {
117
+ backendFilters.push({
118
+ field: key,
119
+ value: this.transformFilterValue(String(filterMeta.value)),
120
+ operator: this.transformOperator(filterMeta.matchMode)
121
+ });
122
+ }
123
+ });
124
+
125
+ console.log('Converted filters:', backendFilters);
126
+ return backendFilters;
127
+ }
128
+
129
+ /**
130
+ * Transform filter value based on backend requirements - all values must be strings
131
+ */
132
+ protected transformFilterValue(value: any): string {
133
+ if (value === null || value === undefined) {
134
+ return '';
135
+ }
136
+ if (typeof value === 'string') {
137
+ return value.trim();
138
+ }
139
+ if (Array.isArray(value)) {
140
+ return value.join(',');
141
+ }
142
+ if (value instanceof Date) {
143
+ return value.toISOString();
144
+ }
145
+ if (typeof value === 'number' || typeof value === 'boolean') {
146
+ return value.toString();
147
+ }
148
+ return String(value);
149
+ }
150
+
151
+ /**
152
+ * Map PrimeNG operators to backend operators
153
+ */
154
+ protected transformOperator(operator: string): FilterOperator {
155
+ const operatorMap: { [key: string]: FilterOperator } = {
156
+ 'contains': 'CONTAINS',
157
+ 'startsWith': 'STARTS_WITH',
158
+ 'endsWith': 'ENDS_WITH',
159
+ 'equals': 'EQUALS',
160
+ 'notEquals': 'NOT_EQUALS',
161
+ 'gt': 'GREATER_THAN',
162
+ 'gte': 'GREATER_THAN_EQUALS',
163
+ 'lt': 'LESS_THAN',
164
+ 'lte': 'LESS_THAN_EQUALS'
165
+ };
166
+ return operatorMap[operator] || 'EQUALS'; // Default to EQUALS if operator not found
167
+ }
168
+
169
+ loadData(event?: TableLazyLoadEvent) {
170
+ console.log('LoadData called with event:', event);
171
+ this.loading = true;
172
+
173
+ // Create page request with default values
174
+ const pageRequest: PageRequest = {
175
+ page: 0,
176
+ size: 10,
177
+ sorts: [],
178
+ filters: []
179
+ };
180
+
181
+ if (event) {
182
+ // Handle pagination
183
+ if (event.first !== undefined && event.rows) {
184
+ pageRequest.page = Math.floor(event.first / event.rows);
185
+ pageRequest.size = event.rows;
186
+ }
187
+
188
+ // Handle sorting
189
+ if (event.sortField) {
190
+ pageRequest.sorts = [{
191
+ field: Array.isArray(event.sortField) ? event.sortField[0] : event.sortField,
192
+ direction: event.sortOrder === 1 ? 'asc' : 'desc'
193
+ }];
194
+ }
195
+
196
+ // Handle filtering
197
+ if (event.filters) {
198
+ const convertedFilters = this.convertFilters(event.filters);
199
+ console.log('Converted filters:', convertedFilters);
200
+ pageRequest.filters = convertedFilters;
201
+ }
202
+ }
203
+
204
+ console.log('Sending page request:', pageRequest);
205
+
206
+ this.getEntityService().getAll(pageRequest)
207
+ .pipe(finalize(() => this.loading = false))
208
+ .subscribe({
209
+ next: (response: Page<T>) => {
210
+ this.items = [...response.content];
211
+ this.totalRecords = response.page.totalElements;
212
+ },
213
+ error: (error) => {
214
+ console.error('Failed to load data:', error);
215
+ this.showError(`${AppMessages.LIST.ERROR.LOAD} ${this.getEntityName()}`);
216
+ }
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Navigate to create a new entity
222
+ */
223
+ public onCreate(): void {
224
+ this.navigateTo([this.getFormRoute(), AppConstants.PAGE_MODE.CREATE]);
225
+ }
226
+
227
+ /**
228
+ * Navigate to view an entity
229
+ * @param item The entity to view
230
+ */
231
+ onView(item: T) {
232
+ if (!this.validateItemUuid(item, 'view')) return;
233
+ this.navigateTo([this.getFormRoute(), AppConstants.PAGE_MODE.VIEW], { uuid: item.uuid });
234
+ }
235
+
236
+ /**
237
+ * Navigate to edit an entity
238
+ * @param item The entity to edit
239
+ */
240
+ onEdit(item: T) {
241
+ if (!this.validateItemUuid(item, 'edit')) return;
242
+ this.navigateTo([this.getFormRoute(), AppConstants.PAGE_MODE.EDIT], { uuid: item.uuid });
243
+ }
244
+
245
+ /**
246
+ * Navigate to a route with error handling
247
+ * @param path The route path
248
+ * @param matrixParams Optional matrix parameters
249
+ */
250
+ protected navigateTo(path: any[], matrixParams?: { [key: string]: any }): void {
251
+ this.router.navigate(path, { queryParams: matrixParams })
252
+ .catch(error => {
253
+ console.error('Navigation error:', error);
254
+ this.showError(AppMessages.LIST.ERROR.NAVIGATION);
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Delete an entity with confirmation
260
+ * @param item The entity to delete
261
+ */
262
+ onDelete(item: T) {
263
+ if (!this.validateItemUuid(item, 'delete')) return;
264
+
265
+ this.confirmationService.confirm({
266
+ message: `Are you sure you want to delete this ${this.getEntityName()}?`,
267
+ header: 'Confirm Deletion',
268
+ icon: 'pi pi-exclamation-triangle',
269
+ accept: () => {
270
+ this.actionLoading = true;
271
+ this.getEntityService().delete(item.uuid)
272
+ .pipe(finalize(() => this.actionLoading = false))
273
+ .subscribe({
274
+ next: () => {
275
+ this.showSuccess(`${this.getEntityName()} ${AppMessages.LIST.SUCCESS.DELETE}`);
276
+ this.loadData();
277
+ },
278
+ error: (error) => {
279
+ console.error('Delete failed:', error);
280
+ this.showError(`${AppMessages.LIST.ERROR.DELETE} ${this.getEntityName()}`);
281
+ }
282
+ });
283
+ }
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Validate that an entity has a UUID
289
+ * @param item The entity to validate
290
+ * @param action The action being performed
291
+ * @returns true if valid, false otherwise
292
+ */
293
+ protected validateItemUuid(item: T, action: string): boolean {
294
+ if (!item?.uuid) {
295
+ this.showError(`${AppMessages.LIST.ERROR.INVALID_ID} ${action}`);
296
+ return false;
297
+ }
298
+ return true;
299
+ }
300
+
301
+ /**
302
+ * Get an observable for loading state from the entity service
303
+ * @returns An observable of loading state if available, null otherwise
304
+ */
305
+ protected getLoadingObservable(): Observable<boolean> | null {
306
+ const service = this.getEntityService() as any;
307
+ return service.loading$ || null;
308
+ }
309
+
310
+ /**
311
+ * Get the entity service for data operations
312
+ */
313
+ protected abstract getEntityService(): {
314
+ getAll: (pageRequest?: PageRequest) => Observable<Page<T>>;
315
+ delete: (uuid: string) => Observable<any>;
316
+ };
317
+
318
+ /**
319
+ * Get the entity name for display in messages
320
+ */
321
+ protected abstract getEntityName(): string;
322
+
323
+ /**
324
+ * Get the route to the entity form
325
+ */
326
+ protected abstract getFormRoute(): string;
327
+ }
@@ -0,0 +1,33 @@
1
+ import { Directive } from '@angular/core';
2
+ import { UtilsService } from '../services/utils.service';
3
+ import { NotificationService } from '../services/notification.service';
4
+
5
+ /**
6
+ * Base component for all page components in the application.
7
+ * Provides common functionality like loading state and message display.
8
+ */
9
+ @Directive()
10
+ export abstract class BasePageComponent {
11
+ protected loading: boolean = false;
12
+
13
+ constructor(
14
+ protected utilsService: UtilsService,
15
+ protected notificationService: NotificationService
16
+ ) {}
17
+
18
+ /**
19
+ * Display an error message to the user
20
+ * @param detail The error message to display
21
+ */
22
+ protected showError(detail: string): void {
23
+ this.notificationService.showError(detail);
24
+ }
25
+
26
+ /**
27
+ * Display a success message to the user
28
+ * @param detail The success message to display
29
+ */
30
+ protected showSuccess(detail: string): void {
31
+ this.notificationService.showSuccess(detail);
32
+ }
33
+ }
@@ -0,0 +1,345 @@
1
+ import { Component, OnInit, Injectable } from '@angular/core';
2
+ import { ActivatedRoute, Router } from '@angular/router';
3
+ import { ComponentContext } from '../enums/component-context.enum';
4
+ import { BasePageComponent } from './base-page.component';
5
+ import { UtilsService } from '../services/utils.service';
6
+ import { NotificationService } from '../services/notification.service';
7
+ import { AppMessages } from '../constants/app.messages';
8
+ import { AppConstants, PageMode } from '../constants/app.constants';
9
+ import { Observable, take, map } from 'rxjs';
10
+ import { EntityService } from '../services/entity.service';
11
+ import { TabbedFormType } from '../enums/tabbed-form-type.enum';
12
+
13
+ export interface TabConfig {
14
+ value: string;
15
+ title: string;
16
+ isParentModel?: boolean;
17
+ }
18
+
19
+ @Injectable()
20
+ @Component({
21
+ template: ''
22
+ })
23
+ export abstract class BaseTabbedFormComponent<T extends { uuid: string }>
24
+ extends BasePageComponent implements OnInit {
25
+
26
+ // Tab specific properties
27
+ activeTab: string = '';
28
+ tabs: TabConfig[] = [];
29
+ formType = '';
30
+
31
+ // Form properties
32
+ isNew = true;
33
+ mode: PageMode = AppConstants.PAGE_MODE.CREATE;
34
+ currentFormData!: T;
35
+ protected originalFormData: T | null = null;
36
+ protected hasChanges: boolean = false;
37
+
38
+ private debug(message: string, ...args: any[]): void {
39
+ // Only log in development
40
+ if (process.env['NODE_ENV'] !== 'production') {
41
+ console.log(`[BaseTabbedFormComponent] ${message}`, ...args);
42
+ }
43
+ }
44
+
45
+ constructor(
46
+ protected route: ActivatedRoute,
47
+ protected router: Router,
48
+ protected override notificationService: NotificationService,
49
+ protected override utilsService: UtilsService
50
+ ) {
51
+ super(utilsService, notificationService);
52
+ }
53
+
54
+ ngOnInit() {
55
+ try {
56
+ // Initialize tabs
57
+ this.tabs = this.getTabs();
58
+ this.activeTab = this.tabs[0]?.value || '';
59
+
60
+ // Initialize form using query params
61
+ this.route.queryParams.pipe(take(1)).subscribe({
62
+ next: params => this.initializeForm(params),
63
+ error: error => {
64
+ console.error('Error initializing form:', error);
65
+ this.showError(AppMessages.FORM.ERROR.INIT);
66
+ }
67
+ });
68
+ } catch (error) {
69
+ console.error('Error in ngOnInit:', error);
70
+ this.showError(AppMessages.FORM.ERROR.INIT);
71
+ }
72
+ }
73
+
74
+
75
+ private initializeFormData() {
76
+ this.debug('Initializing form data');
77
+ this.debug('Current mode:', this.mode);
78
+
79
+ if (this.mode === AppConstants.PAGE_MODE.CREATE) {
80
+ this.debug('Create mode, using defaults');
81
+ this.loadDefaultTab();
82
+ return;
83
+ }
84
+
85
+ const state = {
86
+ ...this.router.getCurrentNavigation()?.extras?.state,
87
+ ...window.history.state
88
+ } as { formData?: T };
89
+
90
+ this.debug('Combined state:', state);
91
+
92
+ if (state?.formData) {
93
+ this.debug('Found form data in state:', state.formData);
94
+ this.patchFormData(state.formData);
95
+ this.loadDefaultTab();
96
+ } else {
97
+ this.debug('No form data, loading from UUID');
98
+ this.loadFromUuid();
99
+ }
100
+ }
101
+
102
+ private loadFromUuid() {
103
+ // Flatten nested subscriptions using RxJS operators
104
+ this.route.queryParams.pipe(
105
+ take(1),
106
+ map((params: { [key: string]: string }) => {
107
+ const uuid = params[AppConstants.FORM.UUID];
108
+ if (!uuid) {
109
+ throw new Error(`UUID is required for ${this.mode} mode when no formData is provided`);
110
+ }
111
+ return uuid;
112
+ })
113
+ ).subscribe({
114
+ next: (uuid: string) => this.loadEntity(uuid),
115
+ error: (error: Error) => {
116
+ console.error('Error loading UUID:', error);
117
+ this.showError(`${AppMessages.FORM.ERROR.LOAD} UUID`);
118
+ }
119
+ });
120
+ }
121
+
122
+ private initializeForm(params: any) {
123
+ // Remove debug logging for production
124
+ const routeMode = params[AppConstants.FORM.MODE];
125
+
126
+ try {
127
+ this.initializeFormMode(routeMode);
128
+ this.setupFormState();
129
+
130
+ if (this.formType === TabbedFormType.LIST) {
131
+ this.initializeListForm(params);
132
+ } else {
133
+ this.initializeFormData();
134
+ }
135
+ } catch (error) {
136
+ console.error('Error initializing form:', error);
137
+ this.showError(`${AppMessages.FORM.ERROR.INIT}: ${error instanceof Error ? error.message : 'Unknown error'}`);
138
+ }
139
+ }
140
+
141
+ protected patchFormData(data: T | null) {
142
+ if (data) {
143
+ this.currentFormData = { ...data };
144
+ this.originalFormData = { ...data }; // Create a deep copy to avoid reference issues
145
+ this.isNew = false;
146
+ }
147
+ }
148
+
149
+ private initializeFormMode(routeMode: string | undefined) {
150
+ if (!routeMode) {
151
+ this.mode = AppConstants.PAGE_MODE.CREATE;
152
+ return;
153
+ }
154
+
155
+ // Validate mode more explicitly
156
+ if (!this.isValidPageMode(routeMode)) {
157
+ const validModes = Object.values(AppConstants.PAGE_MODE).join(', ');
158
+ throw new Error(`Invalid page mode: ${routeMode}. Valid modes are: ${validModes}`);
159
+ }
160
+
161
+ this.mode = routeMode;
162
+ }
163
+
164
+ private setupFormState() {
165
+ this.debug('Setting up form state, mode:', this.mode);
166
+
167
+ if (this.mode === AppConstants.PAGE_MODE.CREATE) {
168
+ this.debug('Create mode setup');
169
+ this.isNew = true;
170
+ this.loading = false;
171
+ this.originalFormData = null;
172
+ }
173
+ }
174
+
175
+ protected loadEntity(uuid: string) {
176
+ this.loading = true;
177
+ this.isNew = false;
178
+ const entityName = this.getEntityName();
179
+
180
+ this.getEntityService().getById(uuid).subscribe({
181
+ next: (data: T) => {
182
+ this.originalFormData = data;
183
+ this.currentFormData = { ...data };
184
+ this.loading = false;
185
+ this.debug('Original data loaded:', this.originalFormData);
186
+ this.debug('Form data updated:', this.currentFormData);
187
+ this.loadDefaultTab();
188
+ },
189
+ error: (error: Error) => {
190
+ this.debug('Error loading entity:', error);
191
+ this.showError(`${AppMessages.FORM.ERROR.LOAD} ${entityName}`);
192
+ this.loading = false;
193
+ }
194
+ });
195
+ }
196
+
197
+ protected findTabByValue(value: string): TabConfig | undefined {
198
+ return this.tabs.find(tab => tab.value === value);
199
+ }
200
+
201
+ protected loadDefaultTab() {
202
+ this.debug('Loading default tab');
203
+ const defaultTab = this.tabs.find(tab => tab.isParentModel) || this.tabs[0];
204
+
205
+ if (!defaultTab) return;
206
+
207
+ if (this.formType === TabbedFormType.LIST) {
208
+ const state = this.router.getCurrentNavigation()?.extras?.state || window.history.state;
209
+ const formDataList = state?.formDataList || [];
210
+ const selectedData = formDataList.find((data: T) => data.uuid === defaultTab.value);
211
+
212
+ this.router.navigate([defaultTab.value], {
213
+ relativeTo: this.route,
214
+ state: {
215
+ formData: selectedData,
216
+ formDataList: formDataList,
217
+ context: ComponentContext.TABBED
218
+ }
219
+ });
220
+ } else {
221
+ this.router.navigate([defaultTab.value], {
222
+ relativeTo: this.route,
223
+ queryParamsHandling: 'preserve', // Keep existing query params (mode & uuid)
224
+ state: {
225
+ formData: this.currentFormData,
226
+ context: ComponentContext.TABBED
227
+ }
228
+ });
229
+ }
230
+ }
231
+
232
+ protected getTabFormData(tabConfig: TabConfig): T | null {
233
+ if (!tabConfig) return null;
234
+
235
+ if (tabConfig.isParentModel) {
236
+ return this.currentFormData;
237
+ }
238
+
239
+ const fieldName = this.utilsService.toCamelCase(tabConfig.value);
240
+ // Use type assertion since we know this is a valid field
241
+ return (this.currentFormData as any)[fieldName] as T;
242
+ }
243
+
244
+ private initializeListForm(params: any) {
245
+ const state = this.router.getCurrentNavigation()?.extras?.state || window.history.state;
246
+ const formDataList = state?.formDataList;
247
+
248
+ if (!formDataList?.length) {
249
+ throw new Error('formDataList is required in state for LIST type');
250
+ }
251
+
252
+ // Create tabs from list in state
253
+ this.tabs = formDataList.map((data: T) => ({
254
+ value: data.uuid,
255
+ title: this.getTabTitle(data),
256
+ isParentModel: true
257
+ }));
258
+
259
+ // Select tab based on UUID in params or first tab
260
+ const uuid = params[AppConstants.FORM.UUID];
261
+ this.activeTab = uuid && this.tabs.find(t => t.value === uuid)
262
+ ? uuid
263
+ : this.tabs[0].value;
264
+
265
+ // Set current form data from list
266
+ const selectedData = formDataList.find((d: T) => d.uuid === this.activeTab);
267
+ this.currentFormData = selectedData;
268
+ this.originalFormData = selectedData;
269
+ }
270
+
271
+ protected onTabChange(value: string | number): void {
272
+ const tabConfig = this.findTabByValue(value as string);
273
+ if (!tabConfig) {
274
+ console.warn(`Tab not found: ${value}`);
275
+ return;
276
+ }
277
+
278
+ this.activeTab = tabConfig.value;
279
+
280
+ if (this.formType === TabbedFormType.LIST) {
281
+ const state = this.router.getCurrentNavigation()?.extras?.state || window.history.state;
282
+ const formDataList = state?.formDataList || [];
283
+ const selectedData = formDataList.find((data: T) => data.uuid === value);
284
+
285
+ // Update current form data
286
+ this.currentFormData = selectedData;
287
+ this.originalFormData = selectedData;
288
+
289
+ this.router.navigate([value], {
290
+ relativeTo: this.route,
291
+ state: {
292
+ formData: selectedData,
293
+ formDataList: formDataList,
294
+ context: ComponentContext.TABBED
295
+ }
296
+ });
297
+ } else {
298
+ const formData = this.getTabFormData(tabConfig);
299
+ this.router.navigate([this.activeTab], {
300
+ relativeTo: this.route,
301
+ queryParamsHandling: 'preserve',
302
+ state: {
303
+ formData,
304
+ context: ComponentContext.TABBED
305
+ }
306
+ });
307
+ }
308
+ }
309
+
310
+ protected hasFormChanges(currentData: T): boolean {
311
+ // For new forms or no original data, consider it changed
312
+ if (this.isNew || !this.originalFormData) {
313
+ return true;
314
+ }
315
+
316
+ // Deep comparison of objects
317
+ return !this.isEqual(currentData, this.originalFormData);
318
+ }
319
+
320
+ private isEqual<V extends Record<string, any>>(obj1: V, obj2: V): boolean {
321
+ if (obj1 === obj2) return true;
322
+ if (typeof obj1 !== 'object' || obj1 === null ||
323
+ typeof obj2 !== 'object' || obj2 === null) return false;
324
+
325
+ const keys1 = Object.keys(obj1);
326
+ const keys2 = Object.keys(obj2);
327
+
328
+ if (keys1.length !== keys2.length) return false;
329
+
330
+ return keys1.every(key =>
331
+ Object.prototype.hasOwnProperty.call(obj2, key) &&
332
+ this.isEqual(obj1[key], obj2[key])
333
+ );
334
+ }
335
+
336
+ private isValidPageMode(mode: string): mode is PageMode {
337
+ return Object.values(AppConstants.PAGE_MODE).includes(mode as PageMode);
338
+ }
339
+
340
+ // Abstract methods that must be implemented by child classes
341
+ protected abstract getTabs(): TabConfig[];
342
+ protected abstract getEntityService(): EntityService<T>;
343
+ protected abstract getEntityName(): string;
344
+ protected abstract getTabTitle(data: T): string;
345
+ }