@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.
- package/dist/core-lib/fesm2022/core-lib.mjs +2349 -0
- package/dist/core-lib/fesm2022/core-lib.mjs.map +1 -0
- package/dist/core-lib/index.d.ts +29 -0
- package/dist/core-lib/lib/components/base-form.component.d.ts +62 -0
- package/dist/core-lib/lib/components/base-list-page.component.d.ts +132 -0
- package/dist/core-lib/lib/components/base-page.component.d.ts +25 -0
- package/dist/core-lib/lib/components/base-tabbed-form.component.d.ts +54 -0
- package/dist/core-lib/lib/components/formly/core-formly.module.d.ts +7 -0
- package/dist/core-lib/lib/components/formly/form-field.wrapper.d.ts +21 -0
- package/dist/core-lib/lib/components/formly/formly.constants.d.ts +13 -0
- package/dist/core-lib/lib/components/formly/types/checkbox.type.d.ts +12 -0
- package/dist/core-lib/lib/components/formly/types/date.type.d.ts +20 -0
- package/dist/core-lib/lib/components/formly/types/multiselect.type.d.ts +26 -0
- package/dist/core-lib/lib/components/formly/types/numeric-input.type.d.ts +17 -0
- package/dist/core-lib/lib/components/formly/types/radio.type.d.ts +13 -0
- package/dist/core-lib/lib/components/formly/types/select.type.d.ts +25 -0
- package/dist/core-lib/lib/components/formly/types/text-input.type.d.ts +12 -0
- package/dist/core-lib/lib/components/formly/types/textarea.type.d.ts +14 -0
- package/dist/core-lib/lib/constants/app.constants.d.ts +13 -0
- package/dist/core-lib/lib/constants/app.messages.d.ts +36 -0
- package/dist/core-lib/lib/enums/component-context.enum.d.ts +6 -0
- package/dist/core-lib/lib/enums/tabbed-form-type.enum.d.ts +4 -0
- package/dist/core-lib/lib/models/pagination/filter-criteria.interface.d.ts +9 -0
- package/dist/core-lib/lib/models/pagination/page-request.interface.d.ts +10 -0
- package/dist/core-lib/lib/models/pagination/page.interface.d.ts +9 -0
- package/dist/core-lib/lib/models/pagination/sort-criteria.interface.d.ts +4 -0
- package/dist/core-lib/lib/pipes/indian-date.pipe.d.ts +12 -0
- package/dist/core-lib/lib/services/base-crud.service.d.ts +36 -0
- package/dist/core-lib/lib/services/entity-registry.service.d.ts +26 -0
- package/dist/core-lib/lib/services/entity.service.d.ts +9 -0
- package/dist/core-lib/lib/services/enum-registry.service.d.ts +41 -0
- package/dist/core-lib/lib/services/formly-config.service.d.ts +11 -0
- package/dist/core-lib/lib/services/http-utility.service.d.ts +130 -0
- package/dist/core-lib/lib/services/notification.service.d.ts +13 -0
- package/dist/core-lib/lib/services/reference-data-provider.service.d.ts +8 -0
- package/dist/core-lib/lib/services/utils.service.d.ts +72 -0
- package/ng-package.json +7 -0
- package/package.json +63 -0
- package/src/index.ts +44 -0
- package/src/lib/components/base-form.component.ts +328 -0
- package/src/lib/components/base-list-page.component.ts +327 -0
- package/src/lib/components/base-page.component.ts +33 -0
- package/src/lib/components/base-tabbed-form.component.ts +345 -0
- package/src/lib/components/formly/core-formly.module.ts +34 -0
- package/src/lib/components/formly/field.interface.ts +15 -0
- package/src/lib/components/formly/form-field.wrapper.ts +94 -0
- package/src/lib/components/formly/formly.constants.ts +14 -0
- package/src/lib/components/formly/primeng-formly.module.ts +77 -0
- package/src/lib/components/formly/types/checkbox.type.ts +43 -0
- package/src/lib/components/formly/types/date.type.ts +88 -0
- package/src/lib/components/formly/types/multiselect.type.ts +61 -0
- package/src/lib/components/formly/types/numeric-input.type.ts +45 -0
- package/src/lib/components/formly/types/radio.type.ts +47 -0
- package/src/lib/components/formly/types/select.type.ts +54 -0
- package/src/lib/components/formly/types/text-input.type.ts +33 -0
- package/src/lib/components/formly/types/textarea.type.ts +37 -0
- package/src/lib/constants/app.constants.ts +15 -0
- package/src/lib/constants/app.messages.ts +36 -0
- package/src/lib/enums/component-context.enum.ts +6 -0
- package/src/lib/enums/tabbed-form-type.enum.ts +4 -0
- package/src/lib/models/pagination/filter-criteria.interface.ts +19 -0
- package/src/lib/models/pagination/page-request.interface.ts +13 -0
- package/src/lib/models/pagination/page.interface.ts +9 -0
- package/src/lib/models/pagination/sort-criteria.interface.ts +4 -0
- package/src/lib/pipes/indian-date.pipe.ts +46 -0
- package/src/lib/services/base-crud.service.ts +81 -0
- package/src/lib/services/entity-registry.service.ts +51 -0
- package/src/lib/services/entity.service.ts +10 -0
- package/src/lib/services/enum-registry.service.ts +66 -0
- package/src/lib/services/formly-config.service.ts +50 -0
- package/src/lib/services/http-utility.service.example.ts +253 -0
- package/src/lib/services/http-utility.service.md +175 -0
- package/src/lib/services/http-utility.service.ts +389 -0
- package/src/lib/services/notification.service.ts +35 -0
- package/src/lib/services/reference-data-provider.service.ts +28 -0
- 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
|
+
}
|