@codex-ts/core-lib 1.0.1 → 1.0.2
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/{fesm2022/codex-ts-core-lib.mjs → dist/core-lib/fesm2022/core-lib.mjs} +1 -1
- package/dist/core-lib/fesm2022/core-lib.mjs.map +1 -0
- package/ng-package.json +7 -0
- package/package.json +63 -42
- 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 +328 -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
- package/fesm2022/codex-ts-core-lib.mjs.map +0 -1
- /package/{index.d.ts → dist/core-lib/index.d.ts} +0 -0
- /package/{lib → dist/core-lib/lib}/components/base-form.component.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/base-list-page.component.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/base-page.component.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/base-tabbed-form.component.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/core-formly.module.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/form-field.wrapper.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/formly.constants.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/checkbox.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/date.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/multiselect.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/numeric-input.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/radio.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/select.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/text-input.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/components/formly/types/textarea.type.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/constants/app.constants.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/constants/app.messages.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/enums/component-context.enum.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/enums/tabbed-form-type.enum.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/models/pagination/filter-criteria.interface.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/models/pagination/page-request.interface.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/models/pagination/page.interface.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/models/pagination/sort-criteria.interface.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/pipes/indian-date.pipe.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/base-crud.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/entity-registry.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/entity.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/enum-registry.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/formly-config.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/http-utility.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/notification.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/reference-data-provider.service.d.ts +0 -0
- /package/{lib → dist/core-lib/lib}/services/utils.service.d.ts +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Component, OnInit, Injectable, Input } from '@angular/core';
|
|
2
|
+
import { ComponentContext } from '../enums/component-context.enum';
|
|
3
|
+
import { FormGroup } from '@angular/forms';
|
|
4
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
5
|
+
import { FormlyFieldConfig } from '@ngx-formly/core';
|
|
6
|
+
import { BasePageComponent } from './base-page.component';
|
|
7
|
+
import { UtilsService } from '../services/utils.service';
|
|
8
|
+
import { NotificationService } from '../services/notification.service';
|
|
9
|
+
import { AppMessages } from '../constants/app.messages';
|
|
10
|
+
import { AppConstants, PageMode } from '../constants/app.constants';
|
|
11
|
+
import { Observable, filter, take } from 'rxjs';
|
|
12
|
+
import { FormlyConfigService } from '../services/formly-config.service';
|
|
13
|
+
import { EntityService } from '../services/entity.service';
|
|
14
|
+
|
|
15
|
+
@Injectable()
|
|
16
|
+
@Component({
|
|
17
|
+
template: ''
|
|
18
|
+
})
|
|
19
|
+
export abstract class BaseFormComponent<T extends { uuid: string }>
|
|
20
|
+
extends BasePageComponent implements OnInit {
|
|
21
|
+
|
|
22
|
+
form = new FormGroup({});
|
|
23
|
+
fields: FormlyFieldConfig[] = [];
|
|
24
|
+
isNew = true;
|
|
25
|
+
mode: PageMode = AppConstants.PAGE_MODE.CREATE;
|
|
26
|
+
currentFormData: any = {};
|
|
27
|
+
protected originalFormData: T | null = null;
|
|
28
|
+
protected hasChanges: boolean = false;
|
|
29
|
+
@Input() context: ComponentContext = ComponentContext.STANDALONE;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
protected route: ActivatedRoute,
|
|
33
|
+
protected router: Router,
|
|
34
|
+
protected override notificationService: NotificationService,
|
|
35
|
+
protected override utilsService: UtilsService,
|
|
36
|
+
protected formlyConfigService: FormlyConfigService
|
|
37
|
+
) {
|
|
38
|
+
super(utilsService, notificationService);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ngOnInit() {
|
|
42
|
+
try {
|
|
43
|
+
this.fields = this.getFormlyFieldConfig();
|
|
44
|
+
|
|
45
|
+
this.form.valueChanges.subscribe(() => {
|
|
46
|
+
console.log('[BaseFormComponent] Form value changed:', this.form.value);
|
|
47
|
+
this.currentFormData = { ...this.form.value };
|
|
48
|
+
this.hasChanges = this.hasFormChanges(this.currentFormData);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Use queryParams instead of params
|
|
52
|
+
this.route.queryParams.pipe(take(1)).subscribe({
|
|
53
|
+
next: params => this.initializeForm(params),
|
|
54
|
+
error: error => {
|
|
55
|
+
console.error('Error initializing form:', error);
|
|
56
|
+
this.showError(AppMessages.FORM.ERROR.INIT);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error in ngOnInit:', error);
|
|
61
|
+
this.showError(AppMessages.FORM.ERROR.INIT);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private initializeFormData() {
|
|
66
|
+
console.log('[BaseFormComponent] Initializing form data');
|
|
67
|
+
console.log('[BaseFormComponent] Current mode:', this.mode);
|
|
68
|
+
|
|
69
|
+
if (this.mode === AppConstants.PAGE_MODE.CREATE) {
|
|
70
|
+
console.log('[BaseFormComponent] Create mode, using defaults');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const state = {
|
|
75
|
+
...this.router.getCurrentNavigation()?.extras?.state,
|
|
76
|
+
...window.history.state,
|
|
77
|
+
...this.route.snapshot.data
|
|
78
|
+
};
|
|
79
|
+
console.log('[BaseFormComponent] Combined state:', state);
|
|
80
|
+
console.log('[BaseFormComponent] Final state being used:', state);
|
|
81
|
+
|
|
82
|
+
if (state && 'formData' in state) {
|
|
83
|
+
console.log('[BaseFormComponent] Found form data in state:', state['formData']);
|
|
84
|
+
this.patchFormData(state['formData']);
|
|
85
|
+
} else {
|
|
86
|
+
console.log('[BaseFormComponent] No form data, loading from UUID');
|
|
87
|
+
this.loadFromUuid();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private loadFromUuid() {
|
|
92
|
+
this.route.queryParams.pipe(take(1)).subscribe(params => {
|
|
93
|
+
const uuid = params[AppConstants.FORM.UUID];
|
|
94
|
+
if (!uuid) {
|
|
95
|
+
throw new Error(`UUID is required for ${this.mode} mode when no formData is provided`);
|
|
96
|
+
}
|
|
97
|
+
this.loadEntity(uuid);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private initializeForm(params: any) {
|
|
102
|
+
console.log('[BaseFormComponent] Initializing form with params:', params);
|
|
103
|
+
const routeMode = params[AppConstants.FORM.MODE];
|
|
104
|
+
this.initializeFormMode(routeMode);
|
|
105
|
+
this.setupFormState();
|
|
106
|
+
|
|
107
|
+
// Check for context in router state
|
|
108
|
+
const state = window.history.state;
|
|
109
|
+
if (state?.context) {
|
|
110
|
+
this.context = state.context;
|
|
111
|
+
console.log('[BaseFormComponent] Context set from router state:', this.context);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.initializeFormData();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected patchFormData(data: any) {
|
|
118
|
+
console.log('[BaseFormComponent] Patching form data:', data);
|
|
119
|
+
if (data) {
|
|
120
|
+
if (!this.fields || this.fields.length === 0) {
|
|
121
|
+
console.warn('[BaseFormComponent] Fields not initialized yet, reinitializing');
|
|
122
|
+
this.fields = this.getFormlyFieldConfig();
|
|
123
|
+
}
|
|
124
|
+
this.currentFormData = { ...data };
|
|
125
|
+
this.originalFormData = data;
|
|
126
|
+
this.isNew = false;
|
|
127
|
+
|
|
128
|
+
console.log('[BaseFormComponent] Form data updated:', this.currentFormData);
|
|
129
|
+
console.log('[BaseFormComponent] Fields available:', this.fields);
|
|
130
|
+
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (!this.form.valid) {
|
|
133
|
+
console.warn('[BaseFormComponent] Form validation issues after data patch');
|
|
134
|
+
}
|
|
135
|
+
console.log('[BaseFormComponent] Form state after sync:', {
|
|
136
|
+
valid: this.form.valid,
|
|
137
|
+
values: this.form.value,
|
|
138
|
+
currentData: this.currentFormData
|
|
139
|
+
});
|
|
140
|
+
}, 100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private initializeFormMode(routeMode: string | undefined) {
|
|
145
|
+
if (!routeMode) {
|
|
146
|
+
this.mode = AppConstants.PAGE_MODE.CREATE;
|
|
147
|
+
} else if (this.isValidPageMode(routeMode)) {
|
|
148
|
+
this.mode = routeMode;
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error(`Invalid page mode: ${routeMode}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private setupFormState() {
|
|
155
|
+
console.log('[BaseFormComponent] Setting up form state, mode:', this.mode);
|
|
156
|
+
this.fields = this.getFormlyFieldConfig();
|
|
157
|
+
|
|
158
|
+
if (this.mode === AppConstants.PAGE_MODE.CREATE) {
|
|
159
|
+
console.log('[BaseFormComponent] Create mode setup');
|
|
160
|
+
this.isNew = true;
|
|
161
|
+
this.loading = false;
|
|
162
|
+
this.originalFormData = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.mode === AppConstants.PAGE_MODE.VIEW) {
|
|
166
|
+
console.log('[BaseFormComponent] View mode setup');
|
|
167
|
+
this.form.disable();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected getFormlyFieldConfig(): FormlyFieldConfig[] {
|
|
172
|
+
return this.formlyConfigService.getFormConfig(
|
|
173
|
+
this.getJsonFields(),
|
|
174
|
+
this.mode,
|
|
175
|
+
this.getDropdownOptions()
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected onSubmitRoute(): string[] {
|
|
180
|
+
return [`/${this.getEntityName().toLowerCase()}s`];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected getEntityOperations(): {
|
|
184
|
+
create: (data: Omit<T, typeof AppConstants.FORM.UUID>) => Observable<T>;
|
|
185
|
+
update: (uuid: string, data: Omit<T, typeof AppConstants.FORM.UUID>) => Observable<T>;
|
|
186
|
+
getById: (uuid: string) => Observable<T>;
|
|
187
|
+
} {
|
|
188
|
+
const entityService = this.getEntityService();
|
|
189
|
+
return {
|
|
190
|
+
create: (data) => entityService.create(data),
|
|
191
|
+
update: (uuid, data) => entityService.update(uuid, { ...data, uuid } as T),
|
|
192
|
+
getById: (uuid) => entityService.getById(uuid)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected onSubmit() {
|
|
197
|
+
console.log('[BaseFormComponent] Form submission:', {
|
|
198
|
+
formValid: this.form.valid,
|
|
199
|
+
formData: this.currentFormData,
|
|
200
|
+
formState: this.form.value
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!this.currentFormData || Object.keys(this.currentFormData).length === 0) {
|
|
204
|
+
this.notificationService.showError('No data to save');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.form.valid) {
|
|
209
|
+
if (this.isNew) {
|
|
210
|
+
console.log('[BaseFormComponent] Saving new entity');
|
|
211
|
+
this.saveEntity();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const hasChanges = this.hasFormChanges(this.currentFormData);
|
|
216
|
+
console.log('[BaseFormComponent] Change detection:', {
|
|
217
|
+
hasChanges,
|
|
218
|
+
currentData: this.currentFormData,
|
|
219
|
+
originalData: this.originalFormData
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!hasChanges) {
|
|
223
|
+
this.notificationService.showSuccess('No changes to save');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log('[BaseFormComponent] Saving changes');
|
|
228
|
+
this.saveEntity();
|
|
229
|
+
} else {
|
|
230
|
+
console.warn('[BaseFormComponent] Form validation failed:', this.form.errors);
|
|
231
|
+
this.notificationService.showError('Please check form errors');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected hasFormChanges(currentData: any): boolean {
|
|
236
|
+
if (!this.originalFormData || this.isNew) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('[BaseFormComponent] Checking for changes:', {
|
|
241
|
+
currentData,
|
|
242
|
+
originalData: this.originalFormData
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const hasChanges = Object.keys(currentData).some(key => {
|
|
246
|
+
const currentValue = currentData[key];
|
|
247
|
+
const originalValue = (this.originalFormData as any)[key];
|
|
248
|
+
const changed = currentValue !== originalValue;
|
|
249
|
+
|
|
250
|
+
if (changed) {
|
|
251
|
+
console.log(`[BaseFormComponent] Field '${key}' changed:`, {
|
|
252
|
+
from: originalValue,
|
|
253
|
+
to: currentValue
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return changed;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
console.log('[BaseFormComponent] Changes detected:', hasChanges);
|
|
261
|
+
return hasChanges;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected saveEntity() {
|
|
265
|
+
this.loading = true;
|
|
266
|
+
const formData = this.currentFormData as Omit<T, typeof AppConstants.FORM.UUID>;
|
|
267
|
+
const entityName = this.getEntityName();
|
|
268
|
+
const entityOperations = this.getEntityOperations();
|
|
269
|
+
|
|
270
|
+
const request$ = this.isNew
|
|
271
|
+
? entityOperations.create(formData)
|
|
272
|
+
: entityOperations.update(this.originalFormData!.uuid, formData);
|
|
273
|
+
|
|
274
|
+
request$.subscribe({
|
|
275
|
+
next: () => {
|
|
276
|
+
this.loading = false;
|
|
277
|
+
this.notificationService.showSuccess(`${entityName} ${AppMessages.FORM.SUCCESS.SAVE}`);
|
|
278
|
+
this.router.navigate(this.onSubmitRoute());
|
|
279
|
+
},
|
|
280
|
+
error: (error: any) => {
|
|
281
|
+
const errorMessage = error.error?.message || `${AppMessages.FORM.ERROR.SAVE} ${entityName}`;
|
|
282
|
+
this.showError(errorMessage);
|
|
283
|
+
this.loading = false;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
protected loadEntity(uuid: string) {
|
|
289
|
+
this.loading = true;
|
|
290
|
+
this.isNew = false;
|
|
291
|
+
const entityName = this.getEntityName();
|
|
292
|
+
|
|
293
|
+
this.getEntityOperations().getById(uuid).subscribe({
|
|
294
|
+
next: (data: T) => {
|
|
295
|
+
this.originalFormData = data;
|
|
296
|
+
this.currentFormData = { ...data };
|
|
297
|
+
this.loading = false;
|
|
298
|
+
console.log('[BaseFormComponent] Original data loaded:', this.originalFormData);
|
|
299
|
+
console.log('[BaseFormComponent] Form data updated:', this.currentFormData);
|
|
300
|
+
},
|
|
301
|
+
error: () => {
|
|
302
|
+
this.showError(`${AppMessages.FORM.ERROR.LOAD} ${entityName}`);
|
|
303
|
+
this.loading = false;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
protected abstract getEntityService(): EntityService<T>;
|
|
309
|
+
|
|
310
|
+
private isValidPageMode(mode: string): mode is PageMode {
|
|
311
|
+
return Object.values(AppConstants.PAGE_MODE).includes(mode as PageMode);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
protected getDropdownOptions(): Record<string, any> {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
protected abstract getJsonFields(): any;
|
|
319
|
+
protected abstract getEntityName(): string;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Checks if the component is running in a specific context
|
|
323
|
+
* @param contextType The context type to check against
|
|
324
|
+
*/
|
|
325
|
+
protected isInContext(contextType: ComponentContext): boolean {
|
|
326
|
+
return this.context === contextType;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
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
|
+
// Navigate to the form route and pass the mode as a query parameter
|
|
225
|
+
this.navigateTo([this.getFormRoute()], { [AppConstants.FORM.MODE]: AppConstants.PAGE_MODE.CREATE });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Navigate to view an entity
|
|
230
|
+
* @param item The entity to view
|
|
231
|
+
*/
|
|
232
|
+
onView(item: T) {
|
|
233
|
+
if (!this.validateItemUuid(item, 'view')) return;
|
|
234
|
+
this.navigateTo([this.getFormRoute(), AppConstants.PAGE_MODE.VIEW], { uuid: item.uuid });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Navigate to edit an entity
|
|
239
|
+
* @param item The entity to edit
|
|
240
|
+
*/
|
|
241
|
+
onEdit(item: T) {
|
|
242
|
+
if (!this.validateItemUuid(item, 'edit')) return;
|
|
243
|
+
this.navigateTo([this.getFormRoute(), AppConstants.PAGE_MODE.EDIT], { uuid: item.uuid });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Navigate to a route with error handling
|
|
248
|
+
* @param path The route path
|
|
249
|
+
* @param matrixParams Optional matrix parameters
|
|
250
|
+
*/
|
|
251
|
+
protected navigateTo(path: any[], matrixParams?: { [key: string]: any }): void {
|
|
252
|
+
this.router.navigate(path, { queryParams: matrixParams })
|
|
253
|
+
.catch(error => {
|
|
254
|
+
console.error('Navigation error:', error);
|
|
255
|
+
this.showError(AppMessages.LIST.ERROR.NAVIGATION);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Delete an entity with confirmation
|
|
261
|
+
* @param item The entity to delete
|
|
262
|
+
*/
|
|
263
|
+
onDelete(item: T) {
|
|
264
|
+
if (!this.validateItemUuid(item, 'delete')) return;
|
|
265
|
+
|
|
266
|
+
this.confirmationService.confirm({
|
|
267
|
+
message: `Are you sure you want to delete this ${this.getEntityName()}?`,
|
|
268
|
+
header: 'Confirm Deletion',
|
|
269
|
+
icon: 'pi pi-exclamation-triangle',
|
|
270
|
+
accept: () => {
|
|
271
|
+
this.actionLoading = true;
|
|
272
|
+
this.getEntityService().delete(item.uuid)
|
|
273
|
+
.pipe(finalize(() => this.actionLoading = false))
|
|
274
|
+
.subscribe({
|
|
275
|
+
next: () => {
|
|
276
|
+
this.showSuccess(`${this.getEntityName()} ${AppMessages.LIST.SUCCESS.DELETE}`);
|
|
277
|
+
this.loadData();
|
|
278
|
+
},
|
|
279
|
+
error: (error) => {
|
|
280
|
+
console.error('Delete failed:', error);
|
|
281
|
+
this.showError(`${AppMessages.LIST.ERROR.DELETE} ${this.getEntityName()}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate that an entity has a UUID
|
|
290
|
+
* @param item The entity to validate
|
|
291
|
+
* @param action The action being performed
|
|
292
|
+
* @returns true if valid, false otherwise
|
|
293
|
+
*/
|
|
294
|
+
protected validateItemUuid(item: T, action: string): boolean {
|
|
295
|
+
if (!item?.uuid) {
|
|
296
|
+
this.showError(`${AppMessages.LIST.ERROR.INVALID_ID} ${action}`);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get an observable for loading state from the entity service
|
|
304
|
+
* @returns An observable of loading state if available, null otherwise
|
|
305
|
+
*/
|
|
306
|
+
protected getLoadingObservable(): Observable<boolean> | null {
|
|
307
|
+
const service = this.getEntityService() as any;
|
|
308
|
+
return service.loading$ || null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get the entity service for data operations
|
|
313
|
+
*/
|
|
314
|
+
protected abstract getEntityService(): {
|
|
315
|
+
getAll: (pageRequest?: PageRequest) => Observable<Page<T>>;
|
|
316
|
+
delete: (uuid: string) => Observable<any>;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get the entity name for display in messages
|
|
321
|
+
*/
|
|
322
|
+
protected abstract getEntityName(): string;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the route to the entity form
|
|
326
|
+
*/
|
|
327
|
+
protected abstract getFormRoute(): string;
|
|
328
|
+
}
|
|
@@ -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
|
+
}
|