@web-portal/core-infrastructure 0.0.1

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 (37) hide show
  1. package/fesm2022/web-portal-core-infrastructure.mjs +2669 -0
  2. package/fesm2022/web-portal-core-infrastructure.mjs.map +1 -0
  3. package/index.d.ts +5 -0
  4. package/lib/guards/auth.guard.d.ts +17 -0
  5. package/lib/interceptors/auth.interceptor.d.ts +2 -0
  6. package/lib/interceptors/duplicate-request.interceptor.d.ts +2 -0
  7. package/lib/interceptors/error.interceptor.d.ts +6 -0
  8. package/lib/interceptors/loading-bar.interceptor.d.ts +2 -0
  9. package/lib/services/api.service.d.ts +193 -0
  10. package/lib/services/app-config.service.d.ts +65 -0
  11. package/lib/services/app-initialize.service.d.ts +14 -0
  12. package/lib/services/auth/auth-dialog.interface.d.ts +13 -0
  13. package/lib/services/auth/auth.service.d.ts +22 -0
  14. package/lib/services/auth/storage.service.d.ts +12 -0
  15. package/lib/services/breakpoint.service.d.ts +20 -0
  16. package/lib/services/cache.service.d.ts +49 -0
  17. package/lib/services/cookie.service.d.ts +12 -0
  18. package/lib/services/document.service.d.ts +9 -0
  19. package/lib/services/file-aia-gcs.service.d.ts +15 -0
  20. package/lib/services/file-gcs.service.d.ts +15 -0
  21. package/lib/services/file-wap-gcs.service.d.ts +31 -0
  22. package/lib/services/file.service.d.ts +17 -0
  23. package/lib/services/loader.service.d.ts +9 -0
  24. package/lib/services/loading-spinner.service.d.ts +9 -0
  25. package/lib/services/loading.service.d.ts +10 -0
  26. package/lib/services/local-storage.service.d.ts +13 -0
  27. package/lib/services/notification.service.d.ts +1 -0
  28. package/lib/services/overlay.service.d.ts +13 -0
  29. package/lib/services/platform.service.d.ts +16 -0
  30. package/lib/services/request-cache.service.d.ts +56 -0
  31. package/lib/services/responsive.service.d.ts +11 -0
  32. package/lib/services/search-event-bridge.service.d.ts +146 -0
  33. package/lib/services/seo.service.d.ts +37 -0
  34. package/lib/services/session-storage.service.d.ts +12 -0
  35. package/lib/services/window.service.d.ts +22 -0
  36. package/package.json +32 -0
  37. package/public-api.d.ts +35 -0
@@ -0,0 +1,2669 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Injectable, PLATFORM_ID as PLATFORM_ID$1, Inject, inject, Optional, Injector, signal } from '@angular/core';
3
+ import * as i1$1 from '@angular/router';
4
+ import { Router, NavigationEnd } from '@angular/router';
5
+ import { BehaviorSubject, distinctUntilChanged, tap, filter, map, catchError, of, throwError, firstValueFrom, Observable, Subject, takeUntil, fromEvent } from 'rxjs';
6
+ import * as i1$2 from '@angular/common/http';
7
+ import { HttpClient, HttpRequest, HttpEventType, HttpHeaders, HttpResponse, HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
8
+ import { BREAKPOINTS_VALUE } from '@web-portal/core-base';
9
+ import * as i1 from '@angular/cdk/layout';
10
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
11
+ import { tap as tap$1, take, catchError as catchError$1, switchMap, finalize, map as map$1 } from 'rxjs/operators';
12
+ import { MessageService } from 'primeng/api';
13
+ import * as i1$3 from 'ngx-cookie-service';
14
+ import moment from 'moment-timezone';
15
+ import { ViewType } from '@web-portal/core-domain';
16
+ import * as i1$4 from '@angular/platform-browser';
17
+
18
+ /**
19
+ * Injection token for AuthDialogService
20
+ * App should provide implementation in app.config.ts
21
+ */
22
+ const AUTH_DIALOG_SERVICE = new InjectionToken('AUTH_DIALOG_SERVICE');
23
+
24
+ class BreakpointService {
25
+ constructor(breakpointObserver) {
26
+ this.breakpointObserver = breakpointObserver;
27
+ this.BreakpointsSubject = new BehaviorSubject({
28
+ isBigDesktopUp: false,
29
+ isDesktopUp: false,
30
+ isPhoneOnly: false,
31
+ isTabletLandscapeUp: false,
32
+ isTabletPortraitUp: false
33
+ });
34
+ this.breakpointsResult$ = this.BreakpointsSubject.asObservable();
35
+ this.breakpoint$ = this.breakpointObserver
36
+ .observe([
37
+ //for-big-desktop-up
38
+ BREAKPOINTS_VALUE.FOR_BIG_DESKTOP_UP,
39
+ //for-desktop-up
40
+ BREAKPOINTS_VALUE.FOR_DESKTOP_UP,
41
+ //for-tablet-landscape-up
42
+ BREAKPOINTS_VALUE.FOR_TABLET_LANDSCAPE_UP,
43
+ //for-tablet-portrait-up
44
+ BREAKPOINTS_VALUE.FOR_TABLET_PORTRAIT_UP,
45
+ //for-phone-only
46
+ BREAKPOINTS_VALUE.FOR_PHONE_ONLY
47
+ ])
48
+ .pipe(distinctUntilChanged());
49
+ this.breakpoint$.subscribe(state => {
50
+ const newBreakpoints = {
51
+ isPhoneOnly: state.breakpoints[BREAKPOINTS_VALUE.FOR_PHONE_ONLY],
52
+ isTabletPortraitUp: state.breakpoints[BREAKPOINTS_VALUE.FOR_TABLET_PORTRAIT_UP],
53
+ isTabletLandscapeUp: state.breakpoints[BREAKPOINTS_VALUE.FOR_TABLET_LANDSCAPE_UP],
54
+ isDesktopUp: state.breakpoints[BREAKPOINTS_VALUE.FOR_DESKTOP_UP],
55
+ isBigDesktopUp: state.breakpoints[BREAKPOINTS_VALUE.FOR_BIG_DESKTOP_UP]
56
+ };
57
+ this.setBreakpoints(newBreakpoints);
58
+ });
59
+ }
60
+ setBreakpoints(breakpoints) {
61
+ this.BreakpointsSubject.next(breakpoints);
62
+ }
63
+ getBreakpoints() {
64
+ return this.BreakpointsSubject.getValue()
65
+ ? this.BreakpointsSubject.getValue()
66
+ : {
67
+ isPhoneOnly: false,
68
+ isTabletPortraitUp: false,
69
+ isTabletLandscapeUp: true,
70
+ isDesktopUp: false,
71
+ isBigDesktopUp: false
72
+ };
73
+ }
74
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreakpointService, deps: [{ token: i1.BreakpointObserver }], target: i0.ɵɵFactoryTarget.Injectable }); }
75
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreakpointService, providedIn: "root" }); }
76
+ }
77
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreakpointService, decorators: [{
78
+ type: Injectable,
79
+ args: [{
80
+ providedIn: "root"
81
+ }]
82
+ }], ctorParameters: () => [{ type: i1.BreakpointObserver }] });
83
+
84
+ // Cache configuration constants
85
+ const DEFAULT_CACHE_TIME = 5 * 60 * 1000; // 5 minutes
86
+ const MAX_CACHE_SIZE = 100;
87
+ class CacheService {
88
+ constructor() {
89
+ this.cache = new Map();
90
+ this.DEFAULT_TTL = DEFAULT_CACHE_TIME;
91
+ this.MAX_CACHE_SIZE = MAX_CACHE_SIZE;
92
+ }
93
+ /**
94
+ * Get data from cache by key
95
+ * @param key Cache key
96
+ * @returns Cached data or null if not found or expired
97
+ */
98
+ get(key) {
99
+ const item = this.cache.get(key);
100
+ if (!item)
101
+ return null;
102
+ const now = Date.now();
103
+ if (now - item.timestamp > this.DEFAULT_TTL) {
104
+ this.cache.delete(key);
105
+ return null;
106
+ }
107
+ // Update last access time
108
+ item.lastAccess = now;
109
+ this.cache.set(key, item);
110
+ return item.data;
111
+ }
112
+ /**
113
+ * Store data in cache
114
+ * @param key Cache key
115
+ * @param data Data to store
116
+ * @param ttl Time to live for the cache entry (in milliseconds)
117
+ */
118
+ set(key, data, ttl = this.DEFAULT_TTL) {
119
+ const now = Date.now();
120
+ // Check if cache has reached size limit
121
+ if (this.cache.size >= this.MAX_CACHE_SIZE && !this.cache.has(key)) {
122
+ this.removeLeastRecentlyUsed();
123
+ }
124
+ this.cache.set(key, {
125
+ data,
126
+ timestamp: now,
127
+ lastAccess: now
128
+ });
129
+ }
130
+ /**
131
+ * Check if cache has valid (non-expired) data for key
132
+ * @param key Cache key
133
+ * @returns True if cache has valid data
134
+ */
135
+ has(key) {
136
+ const item = this.cache.get(key);
137
+ if (!item)
138
+ return false;
139
+ const now = Date.now();
140
+ if (now - item.timestamp > this.DEFAULT_TTL) {
141
+ this.remove(key);
142
+ return false;
143
+ }
144
+ // Update last access time
145
+ item.lastAccess = now;
146
+ this.cache.set(key, item);
147
+ return true;
148
+ }
149
+ /**
150
+ * Remove item from cache by key
151
+ * @param key Cache key
152
+ */
153
+ remove(key) {
154
+ this.cache.delete(key);
155
+ }
156
+ /**
157
+ * Clear all cache data
158
+ */
159
+ clear() {
160
+ this.cache.clear();
161
+ }
162
+ /**
163
+ * Generate a unique key for a given URL and optional parameters
164
+ * @param url Base URL
165
+ * @param params Optional query parameters
166
+ * @returns Generated key
167
+ */
168
+ generateKey(url, params) {
169
+ if (!params)
170
+ return url;
171
+ return `${url}?${JSON.stringify(params)}`;
172
+ }
173
+ /**
174
+ * Remove the least recently used item from cache when it reaches MAX_CACHE_SIZE
175
+ * @private
176
+ */
177
+ removeLeastRecentlyUsed() {
178
+ let oldestKey = null;
179
+ let oldestAccess = Date.now();
180
+ // Find the least recently used item
181
+ this.cache.forEach((value, key) => {
182
+ if (value.lastAccess < oldestAccess) {
183
+ oldestAccess = value.lastAccess;
184
+ oldestKey = key;
185
+ }
186
+ });
187
+ // Remove the least recently used item
188
+ if (oldestKey) {
189
+ this.remove(oldestKey);
190
+ }
191
+ }
192
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
193
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CacheService, providedIn: "root" }); }
194
+ }
195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CacheService, decorators: [{
196
+ type: Injectable,
197
+ args: [{
198
+ providedIn: "root"
199
+ }]
200
+ }], ctorParameters: () => [] });
201
+
202
+ class PlatformService {
203
+ constructor(platformId) {
204
+ this.platformId = platformId;
205
+ }
206
+ get isBrowser() {
207
+ return isPlatformBrowser(this.platformId);
208
+ }
209
+ get isServer() {
210
+ return !isPlatformBrowser(this.platformId);
211
+ }
212
+ runOnBrowser(callback) {
213
+ if (this.isBrowser) {
214
+ return callback();
215
+ }
216
+ return null;
217
+ }
218
+ runOnServer(callback) {
219
+ if (this.isServer) {
220
+ return callback();
221
+ }
222
+ return null;
223
+ }
224
+ getWindow() {
225
+ return this.isBrowser ? window : null;
226
+ }
227
+ getDocument() {
228
+ return this.isBrowser ? document : null;
229
+ }
230
+ getNavigator() {
231
+ return this.isBrowser ? navigator : null;
232
+ }
233
+ getLocalStorage() {
234
+ return this.isBrowser ? localStorage : null;
235
+ }
236
+ getSessionStorage() {
237
+ return this.isBrowser ? sessionStorage : null;
238
+ }
239
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PlatformService, deps: [{ token: PLATFORM_ID$1 }], target: i0.ɵɵFactoryTarget.Injectable }); }
240
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PlatformService, providedIn: "root" }); }
241
+ }
242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PlatformService, decorators: [{
243
+ type: Injectable,
244
+ args: [{ providedIn: "root" }]
245
+ }], ctorParameters: () => [{ type: Object, decorators: [{
246
+ type: Inject,
247
+ args: [PLATFORM_ID$1]
248
+ }] }] });
249
+
250
+ // Constants
251
+ const HTTP_STATUS = {
252
+ OK: 200,
253
+ BAD_REQUEST: 400,
254
+ NOT_ACCEPTABLE: 406
255
+ };
256
+ const ERROR_MESSAGES = {
257
+ INVALID_DATA: "Dữ liệu không hợp lệ",
258
+ INVALID_DATA_RETRY: "Dữ liệu không hợp lệ. Vui lòng thao tác lại"
259
+ };
260
+ const PLATFORM_ID = {
261
+ PHONE: "1",
262
+ DESKTOP: "2"
263
+ };
264
+ const HEADER_GATEWAY_API = "gateway-api";
265
+ /**
266
+ * Injection token cho ApiServiceConfig - chỉ dùng để tránh lỗi DI
267
+ * Thực tế config không được inject, chỉ truyền trực tiếp khi extend
268
+ */
269
+ const API_SERVICE_CONFIG = Symbol("API_SERVICE_CONFIG");
270
+ class ApiService {
271
+ /**
272
+ * Constructor của ApiService
273
+ * @param config - Config cho service (optional khi inject trực tiếp, bắt buộc khi extend)
274
+ * Note: Config không được inject qua DI, chỉ truyền trực tiếp khi extend class
275
+ */
276
+ constructor(config) {
277
+ this.customHeaders = {};
278
+ this._http = inject(HttpClient);
279
+ this._breakpoints = inject(BreakpointService);
280
+ this._cache = inject(CacheService);
281
+ this._platformService = inject(PlatformService);
282
+ // Không inject config qua DI - chỉ nhận khi extend class
283
+ // Khi inject trực tiếp qua DI, config sẽ là undefined
284
+ // Khi extend class, bắt buộc phải truyền config vào super()
285
+ this.isBrowser = this._platformService.isBrowser;
286
+ this._breakpoints.breakpointsResult$.subscribe(observer => {
287
+ this.breakpoints = observer;
288
+ });
289
+ // Tự động initialize nếu có config
290
+ if (config) {
291
+ this._initializeInternal(config);
292
+ }
293
+ }
294
+ /**
295
+ * Initialize service với config - Tự động được gọi trong constructor
296
+ * @protected
297
+ */
298
+ _initializeInternal(config) {
299
+ const parsedConfig = this._parseConfig(config);
300
+ this.baseUrl = parsedConfig.baseUrl;
301
+ this.serviceName = parsedConfig.gatewayApi;
302
+ if (parsedConfig.gatewayApi) {
303
+ this._setGatewayApiHeader(parsedConfig.gatewayApi);
304
+ }
305
+ }
306
+ /**
307
+ * Protected GET request - Sử dụng trong child classes để tránh shadowing
308
+ * @protected
309
+ */
310
+ _get(url, params, useCache = false) {
311
+ return this._handleCachedRequest(() => this.sendRequest("GET", url, null, params), url, params?.toString(), useCache);
312
+ }
313
+ /**
314
+ * Protected GET by ID request
315
+ * @protected
316
+ */
317
+ _getById(url, id, useCache = true) {
318
+ const fullUrl = `${url}/${id}`;
319
+ return this._handleCachedRequest(() => this.sendRequest("GET", fullUrl), fullUrl, undefined, useCache);
320
+ }
321
+ /**
322
+ * Protected DELETE request - Sử dụng trong child classes để tránh shadowing
323
+ * @protected
324
+ */
325
+ _delete(url, id) {
326
+ return this.sendRequest("DELETE", `${url}/${id}`);
327
+ }
328
+ /**
329
+ * Protected PUT request - Sử dụng trong child classes để tránh shadowing
330
+ * @protected
331
+ */
332
+ _put(url, value) {
333
+ return this.sendRequest("PUT", url, value);
334
+ }
335
+ /**
336
+ * Protected POST request - Sử dụng trong child classes để tránh shadowing
337
+ * @protected
338
+ */
339
+ _post(url, value, useCache = false) {
340
+ return this._handleCachedRequest(() => this.sendRequest("POST", url, value), url, value, useCache);
341
+ }
342
+ /**
343
+ * Protected POST FormData request
344
+ * @protected
345
+ */
346
+ _postFormData(url, value) {
347
+ const formData = this._createFormData(value);
348
+ return this._sendFormDataRequest(url, formData, true);
349
+ }
350
+ /**
351
+ * Protected POST File request
352
+ * @protected
353
+ */
354
+ _postFile(url, file, value) {
355
+ const formData = this._createFormData(value);
356
+ formData.append("file", file);
357
+ return this._sendFormDataRequest(url, formData, false);
358
+ }
359
+ // ====== Public methods for backward compatibility ======
360
+ // Các methods này giữ lại để backward compatibility
361
+ // Child classes nên sử dụng protected methods với prefix _
362
+ /**
363
+ * GET request với optional caching
364
+ * @deprecated Sử dụng protected _get() trong child classes để tránh shadowing
365
+ */
366
+ // get<T>(
367
+ // url: string,
368
+ // params?: HttpParams,
369
+ // useCache: boolean = false
370
+ // ): Observable<T> {
371
+ // return this._get<T>(url, params, useCache);
372
+ // }
373
+ // /**
374
+ // * GET request by ID với caching enabled by default
375
+ // * @deprecated Sử dụng protected _getById() trong child classes để tránh shadowing
376
+ // */
377
+ // getById<T>(url: string, id: number, useCache: boolean = true): Observable<T> {
378
+ // return this._getById<T>(url, id, useCache);
379
+ // }
380
+ /**
381
+ * DELETE request
382
+ * @deprecated Sử dụng protected _delete() trong child classes để tránh shadowing
383
+ * Note: Method này đã bị remove để tránh conflict. Sử dụng _delete() trong child classes.
384
+ */
385
+ // delete<T>(url: string, id: string): Observable<T> {
386
+ // return this._delete<T>(url, id);
387
+ // }
388
+ /**
389
+ * PUT request
390
+ * @deprecated Sử dụng protected _put() trong child classes để tránh shadowing
391
+ */
392
+ put(url, value) {
393
+ return this._put(url, value);
394
+ }
395
+ /**
396
+ * POST request với optional caching
397
+ * @deprecated Sử dụng protected _post() trong child classes để tránh shadowing
398
+ */
399
+ post(url, value, useCache = false) {
400
+ return this._post(url, value, useCache);
401
+ }
402
+ /**
403
+ * POST request với FormData
404
+ * @deprecated Sử dụng protected _postFormData() trong child classes để tránh shadowing
405
+ */
406
+ postFormData(url, value) {
407
+ return this._postFormData(url, value);
408
+ }
409
+ /**
410
+ * POST request với File upload
411
+ * @deprecated Sử dụng protected _postFile() trong child classes để tránh shadowing
412
+ */
413
+ postFile(url, file, value) {
414
+ return this._postFile(url, file, value);
415
+ }
416
+ /**
417
+ * Send HTTP request và process response
418
+ * @private
419
+ */
420
+ sendRequest(method, url, data = null, params) {
421
+ const requestOptions = new HttpRequest(method.toUpperCase(), url, data, {
422
+ headers: this.setHeaders(),
423
+ params: params,
424
+ withCredentials: true,
425
+ responseType: "json"
426
+ });
427
+ console.log(`[ApiService] Request: ${method} ${url}`, {
428
+ headers: requestOptions.headers.keys().reduce((acc, key) => ({ ...acc, [key]: requestOptions.headers.get(key) }), {}),
429
+ params: params?.toString(),
430
+ data
431
+ });
432
+ return this._http.request(requestOptions).pipe(tap({
433
+ next: (event) => console.log(`[ApiService] Event for ${url}:`, event),
434
+ error: (error) => console.error(`[ApiService] Request Error for ${url}:`, error)
435
+ }), filter((event) => event?.type === HttpEventType.Response), map((response) => {
436
+ console.log(`[ApiService] Full Response Object for ${url}:`, response);
437
+ return response.body;
438
+ }), tap((apiResponse) => {
439
+ console.log(`[ApiService] Raw Body from ${url}:`, apiResponse);
440
+ if (apiResponse.status !== HTTP_STATUS.OK) {
441
+ throw {
442
+ status: apiResponse.status,
443
+ message: apiResponse.message
444
+ };
445
+ }
446
+ }), map((apiResponse) => apiResponse.data), catchError((error) => {
447
+ console.error(`[ApiService] Caught Error for ${url}:`, error);
448
+ return this._handleError(error);
449
+ }));
450
+ }
451
+ /**
452
+ * Send FormData request
453
+ * @private
454
+ */
455
+ _sendFormDataRequest(url, formData, includeHeaders) {
456
+ const req = new HttpRequest("POST", url, formData, {
457
+ reportProgress: true,
458
+ responseType: "json",
459
+ ...(includeHeaders && { headers: this.setHeaders() })
460
+ });
461
+ return this._http.request(req).pipe(filter((event) => event.type === HttpEventType.Response), map((response) => response.body), tap((apiResponse) => {
462
+ if (apiResponse.status !== HTTP_STATUS.OK) {
463
+ throw {
464
+ status: apiResponse.status,
465
+ message: apiResponse.message
466
+ };
467
+ }
468
+ }), map((apiResponse) => apiResponse.data), catchError((error) => this._handleError(error)));
469
+ }
470
+ /**
471
+ * Get default headers cho mọi request
472
+ * @protected
473
+ */
474
+ getDefaultHeaders() {
475
+ return {
476
+ ConcungContextID: this.generateUUID(),
477
+ ...(this.isBrowser && { returnUrl: window.location.href }),
478
+ Platform_id: this.breakpoints?.isPhoneOnly
479
+ ? PLATFORM_ID.PHONE
480
+ : PLATFORM_ID.DESKTOP
481
+ };
482
+ }
483
+ /**
484
+ * Set headers cho request
485
+ * @protected
486
+ */
487
+ setHeaders(customHeaders) {
488
+ const headersObj = {
489
+ ...this.customHeaders,
490
+ ...this.getDefaultHeaders(),
491
+ ...(customHeaders || {})
492
+ };
493
+ let headers = new HttpHeaders();
494
+ Object.entries(headersObj).forEach(([key, value]) => {
495
+ headers = headers.set(key, value);
496
+ });
497
+ return headers;
498
+ }
499
+ /**
500
+ * Convert object thành query string
501
+ * @param obj - Object chứa key-value pairs
502
+ * @returns Query string
503
+ */
504
+ objectToQueryString(obj) {
505
+ const queryParams = [];
506
+ Object.entries(obj).forEach(([key, value]) => {
507
+ if (value !== undefined && value !== null) {
508
+ const encodedValue = encodeURIComponent(String(value));
509
+ queryParams.push(`${encodeURIComponent(key)}=${encodedValue}`);
510
+ }
511
+ });
512
+ return queryParams.join("&");
513
+ }
514
+ /**
515
+ * Generate UUID v4
516
+ * @returns UUID string without dashes
517
+ */
518
+ generateUUID() {
519
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
520
+ .replace(/[xy]/g, c => {
521
+ const r = (Math.random() * 16) | 0;
522
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
523
+ return v.toString(16);
524
+ })
525
+ .replace(/-/g, "");
526
+ }
527
+ /**
528
+ * Handle cached request logic
529
+ * @private
530
+ */
531
+ _handleCachedRequest(requestFn, url, params, useCache) {
532
+ if (!useCache) {
533
+ return requestFn();
534
+ }
535
+ const cacheKey = this._cache.generateKey(url, params);
536
+ const cachedData = this._cache.get(cacheKey);
537
+ if (cachedData) {
538
+ return of(cachedData);
539
+ }
540
+ return requestFn().pipe(tap(response => {
541
+ this._cache.set(cacheKey, response);
542
+ }));
543
+ }
544
+ /**
545
+ * Create FormData từ object
546
+ * @private
547
+ */
548
+ _createFormData(value) {
549
+ const formData = new FormData();
550
+ if (value) {
551
+ Object.entries(value).forEach(([key, val]) => {
552
+ if (val !== undefined && val !== null) {
553
+ formData.append(key, val);
554
+ }
555
+ });
556
+ }
557
+ return formData;
558
+ }
559
+ /**
560
+ * Handle errors từ HTTP requests
561
+ * @private
562
+ */
563
+ _handleError(error) {
564
+ // Log error in development (environment check removed for library)
565
+ if (typeof console !== 'undefined' && console.error) {
566
+ console.error("API Error:", error);
567
+ }
568
+ const httpError = error;
569
+ // Handle 406 Not Acceptable
570
+ if (httpError?.status === HTTP_STATUS.NOT_ACCEPTABLE) {
571
+ const rawMessage = httpError.error ??
572
+ httpError?.message;
573
+ const message = this._extractErrorMessage(rawMessage, ERROR_MESSAGES.INVALID_DATA);
574
+ return throwError(() => ({
575
+ status: HTTP_STATUS.NOT_ACCEPTABLE,
576
+ message
577
+ }));
578
+ }
579
+ // Handle 400 Bad Request
580
+ if (httpError?.status === HTTP_STATUS.BAD_REQUEST) {
581
+ return throwError(() => ({
582
+ status: httpError.status,
583
+ message: httpError?.error?.message || ERROR_MESSAGES.INVALID_DATA_RETRY
584
+ }));
585
+ }
586
+ // Return original error for other cases
587
+ return throwError(() => error);
588
+ }
589
+ /**
590
+ * Extract error message từ various formats
591
+ * @private
592
+ */
593
+ _extractErrorMessage(raw, fallback) {
594
+ if (typeof raw === "string" && raw.trim().length > 0) {
595
+ return raw;
596
+ }
597
+ if (raw && typeof raw === "object" && "message" in raw) {
598
+ return String(raw.message) || fallback;
599
+ }
600
+ return fallback;
601
+ }
602
+ /**
603
+ * Parse config từ string hoặc object
604
+ * @private
605
+ */
606
+ _parseConfig(config) {
607
+ if (typeof config === "string") {
608
+ return { gatewayApi: config };
609
+ }
610
+ return {
611
+ gatewayApi: config.gatewayApi,
612
+ baseUrl: config.baseUrl
613
+ };
614
+ }
615
+ /**
616
+ * Set gateway-api header cho tất cả requests
617
+ * @private
618
+ */
619
+ _setGatewayApiHeader(serviceName) {
620
+ this.customHeaders = {
621
+ ...this.customHeaders,
622
+ [HEADER_GATEWAY_API]: serviceName
623
+ };
624
+ }
625
+ /**
626
+ * Override gateway-api header cho request cụ thể
627
+ * @param gatewayApi - Gateway API name
628
+ * @returns Instance để support method chaining
629
+ *
630
+ * @example
631
+ * ```typescript
632
+ * this.withGatewayApi('other-api')._get('/endpoint');
633
+ * ```
634
+ */
635
+ withGatewayApi(gatewayApi) {
636
+ this.customHeaders = {
637
+ ...this.customHeaders,
638
+ [HEADER_GATEWAY_API]: gatewayApi
639
+ };
640
+ return this;
641
+ }
642
+ /**
643
+ * Get current service name
644
+ * @returns Gateway API name hoặc undefined
645
+ */
646
+ getServiceName() {
647
+ return this.serviceName;
648
+ }
649
+ /**
650
+ * Get base URL for current service
651
+ * @returns Base URL hoặc undefined
652
+ */
653
+ getBaseUrl() {
654
+ return this.baseUrl;
655
+ }
656
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiService, deps: [{ token: API_SERVICE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
657
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiService, providedIn: "root" }); }
658
+ }
659
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiService, decorators: [{
660
+ type: Injectable,
661
+ args: [{
662
+ providedIn: "root"
663
+ }]
664
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
665
+ type: Optional
666
+ }, {
667
+ type: Inject,
668
+ args: [API_SERVICE_CONFIG]
669
+ }] }] });
670
+
671
+ const USER_KEY = "UserPrincipal";
672
+ const AUTH_EXPIRED_KEY = "__AUTH_EXPIRED__";
673
+ class StorageService {
674
+ clean() {
675
+ window.sessionStorage.clear();
676
+ }
677
+ saveUser(user) {
678
+ window.sessionStorage.removeItem(USER_KEY);
679
+ window.sessionStorage.setItem(USER_KEY, JSON.stringify(user));
680
+ // phiên mới hợp lệ → clear cờ hết hạn
681
+ this.setAuthExpired(false);
682
+ }
683
+ removeUser() {
684
+ window.sessionStorage.removeItem(USER_KEY);
685
+ }
686
+ getUser() {
687
+ const user = window.sessionStorage.getItem(USER_KEY);
688
+ if (user) {
689
+ return JSON.parse(user);
690
+ }
691
+ return null;
692
+ }
693
+ isLoggedIn() {
694
+ const user = window.sessionStorage.getItem(USER_KEY);
695
+ if (user) {
696
+ return true;
697
+ }
698
+ return false;
699
+ }
700
+ setAuthExpired(value) {
701
+ try {
702
+ window.sessionStorage.setItem(AUTH_EXPIRED_KEY, value ? "1" : "0");
703
+ }
704
+ catch { }
705
+ }
706
+ isAuthExpired() {
707
+ try {
708
+ return window.sessionStorage.getItem(AUTH_EXPIRED_KEY) === "1";
709
+ }
710
+ catch {
711
+ return false;
712
+ }
713
+ }
714
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
715
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StorageService, providedIn: "root" }); }
716
+ }
717
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StorageService, decorators: [{
718
+ type: Injectable,
719
+ args: [{
720
+ providedIn: "root"
721
+ }]
722
+ }] });
723
+
724
+ class AuthService extends ApiService {
725
+ getIsReLogin() {
726
+ return this.isReLogin;
727
+ }
728
+ setIsReLogin(value) {
729
+ this.isReLogin = value;
730
+ }
731
+ constructor(_storageService) {
732
+ super({ baseUrl: "/accounts" });
733
+ this._storageService = _storageService;
734
+ this.isReLogin = false;
735
+ }
736
+ async isAuthenticated() {
737
+ var isValidSession = await this.isValidSession();
738
+ if (!isValidSession) {
739
+ this._storageService.removeUser();
740
+ return false;
741
+ }
742
+ return true;
743
+ }
744
+ async isValidSession() {
745
+ try {
746
+ let response = await firstValueFrom(this._post(`${this.baseUrl}/checkLogin`, null));
747
+ const user = response;
748
+ if (user == null || user == undefined || user.UserID <= 0) {
749
+ return false;
750
+ }
751
+ this._user = user;
752
+ this._storageService.saveUser(this._user);
753
+ return true;
754
+ }
755
+ catch (err) {
756
+ console.log("error:", err);
757
+ return false;
758
+ }
759
+ }
760
+ getCurrentUser() {
761
+ var currentUser = this._storageService.getUser();
762
+ return currentUser;
763
+ }
764
+ hasPermission(roleFunctionName) {
765
+ var currentUser = this._storageService.getUser();
766
+ const user = currentUser;
767
+ if (user == null)
768
+ return false;
769
+ return user.UserPermission?.map(x => x.RoleFunctionName).includes(roleFunctionName);
770
+ }
771
+ login(returnUrl) {
772
+ try {
773
+ const url = `${this.baseUrl}/login?returnUrl=${returnUrl}`;
774
+ this._get(url, null, false).subscribe({
775
+ next: (url) => {
776
+ window.location.href = url;
777
+ return;
778
+ },
779
+ error: err => {
780
+ console.log("err:", err);
781
+ }
782
+ });
783
+ }
784
+ catch (err) {
785
+ console.log("error:", err);
786
+ }
787
+ return;
788
+ }
789
+ logout() {
790
+ try {
791
+ this._post(`${this.baseUrl}/logout`).subscribe({
792
+ next: async (url) => {
793
+ this._storageService.clean();
794
+ window.location.href = url;
795
+ },
796
+ error: err => {
797
+ console.log("err:", err);
798
+ }
799
+ });
800
+ return true;
801
+ }
802
+ catch (err) {
803
+ console.log("error:", err);
804
+ }
805
+ return true;
806
+ }
807
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthService, deps: [{ token: StorageService }], target: i0.ɵɵFactoryTarget.Injectable }); }
808
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthService, providedIn: "root" }); }
809
+ }
810
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthService, decorators: [{
811
+ type: Injectable,
812
+ args: [{
813
+ providedIn: "root"
814
+ }]
815
+ }], ctorParameters: () => [{ type: StorageService }] });
816
+ class SessionValidationError extends Error {
817
+ constructor(message) {
818
+ super(message);
819
+ this.name = "SessionValidationError";
820
+ }
821
+ }
822
+
823
+ function isPageReload$1() {
824
+ try {
825
+ return window.sessionStorage.getItem("__LOAD_IS_RELOAD__") === "1";
826
+ }
827
+ catch {
828
+ return false;
829
+ }
830
+ }
831
+ class AuthGuard {
832
+ get authDialog() {
833
+ if (!this._authDialog) {
834
+ this._authDialog = this.injector.get(AUTH_DIALOG_SERVICE, null);
835
+ }
836
+ return this._authDialog;
837
+ }
838
+ constructor(router, _authService, storage, injector) {
839
+ this.router = router;
840
+ this._authService = _authService;
841
+ this.storage = storage;
842
+ this.injector = injector;
843
+ // Lazy inject AuthDialogService to avoid circular dependency
844
+ this._authDialog = null;
845
+ this._authService.setIsReLogin(false);
846
+ }
847
+ async canActivate(route) {
848
+ const requiredPermission = route.data.permission;
849
+ var returnUrl = window.document.URL;
850
+ // Ghi nhận trạng thái có user trong session TRƯỚC khi gọi isAuthenticated()
851
+ const hadSessionUser = this.storage.isLoggedIn();
852
+ var isAuthenticated = await this._authService.isAuthenticated();
853
+ if (isAuthenticated) {
854
+ this._authService.setIsReLogin(true);
855
+ const hasPermission = this._authService.hasPermission(requiredPermission);
856
+ if (!hasPermission) {
857
+ this.router.navigate(["page-403"], { skipLocationChange: true });
858
+ return false;
859
+ }
860
+ return true;
861
+ }
862
+ // Nếu F5/reload → đá thẳng qua login, không popup
863
+ if (isPageReload$1()) {
864
+ this._authService.login(returnUrl);
865
+ return false;
866
+ }
867
+ // Nếu trước đó chưa có user trong session (chưa từng login)
868
+ // → redirect thẳng đến login, không show popup
869
+ if (!hadSessionUser) {
870
+ this._authService.login(returnUrl);
871
+ return false;
872
+ }
873
+ // Nếu đã có user nhưng session hết hạn → show popup re-login
874
+ if (!this.authDialog) {
875
+ // Fallback: redirect to login if dialog service not provided
876
+ this._authService.login(returnUrl);
877
+ return false;
878
+ }
879
+ const confirmed = await this.authDialog.openReLoginDialog();
880
+ if (confirmed) {
881
+ // Mở popup đăng nhập lại, không reload trang hiện tại
882
+ const screenX = typeof window.screenX != "undefined"
883
+ ? window.screenX
884
+ : window.screenLeft;
885
+ const screenY = typeof window.screenY != "undefined"
886
+ ? window.screenY
887
+ : window.screenTop;
888
+ const outerWidth = typeof window.outerWidth != "undefined"
889
+ ? window.outerWidth
890
+ : document.body.clientWidth;
891
+ const outerHeight = typeof window.outerHeight != "undefined"
892
+ ? window.outerHeight
893
+ : document.body.clientHeight - 22;
894
+ const width = 550;
895
+ const height = 600;
896
+ const left = screenX + (outerWidth - width) / 2;
897
+ const top = screenY + (outerHeight - height) / 3;
898
+ const features = [
899
+ `width=${width}`,
900
+ `height=${height}`,
901
+ `left=${left}`,
902
+ `top=${top}`,
903
+ "resizable=yes",
904
+ "scrollbars=yes",
905
+ "status=no",
906
+ "location=no",
907
+ "menubar=no",
908
+ "toolbar=no"
909
+ ].join(",");
910
+ const popupReturnUrl = `${window.document.baseURI}` + "account/relogin";
911
+ window.open(`/accounts/relogin?returnUrl=${popupReturnUrl}`, "Đăng nhập lại", features);
912
+ }
913
+ // Hủy điều hướng hiện tại để không reload
914
+ return false;
915
+ }
916
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthGuard, deps: [{ token: i1$1.Router }, { token: AuthService }, { token: StorageService }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); }
917
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthGuard, providedIn: "root" }); }
918
+ }
919
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthGuard, decorators: [{
920
+ type: Injectable,
921
+ args: [{
922
+ providedIn: "root"
923
+ }]
924
+ }], ctorParameters: () => [{ type: i1$1.Router }, { type: AuthService }, { type: StorageService }, { type: i0.Injector }] });
925
+
926
+ // Helper functions
927
+ let isShowingDialog = false;
928
+ function hasSessionUser() {
929
+ try {
930
+ return !!window.sessionStorage.getItem("UserPrincipal");
931
+ }
932
+ catch {
933
+ return false;
934
+ }
935
+ }
936
+ function isPageReload() {
937
+ try {
938
+ return window.sessionStorage.getItem("__LOAD_IS_RELOAD__") === "1";
939
+ }
940
+ catch {
941
+ return false;
942
+ }
943
+ }
944
+ // Functional interceptor for newer Angular versions
945
+ const authInterceptor = (req, next) => {
946
+ const router = inject(Router);
947
+ const authService = inject(AuthService);
948
+ const storage = inject(StorageService);
949
+ const injector = inject(Injector);
950
+ return next(req).pipe(tap$1(async (response) => {
951
+ if (response instanceof HttpResponse) {
952
+ const body = response.body;
953
+ if (body?.status === 401) {
954
+ await ensureReLoginDialog(injector, authService, storage);
955
+ return;
956
+ }
957
+ }
958
+ }, async (error) => {
959
+ if (error instanceof HttpErrorResponse) {
960
+ const respError = error;
961
+ if (respError.status === 403) {
962
+ router.navigate(["/page-403"]);
963
+ }
964
+ else if (respError.status === 401) {
965
+ await ensureReLoginDialog(injector, authService, storage);
966
+ return;
967
+ }
968
+ }
969
+ }));
970
+ };
971
+ async function ensureReLoginDialog(injector, authService, storage) {
972
+ // Lazy inject AuthDialogService to avoid circular dependency
973
+ const authDialog = injector.get(AUTH_DIALOG_SERVICE, null);
974
+ if (!authDialog) {
975
+ // Fallback: redirect to login if dialog service not provided
976
+ const returnUrl = window.location.href;
977
+ authService.login(returnUrl);
978
+ return;
979
+ }
980
+ // Nếu đang reload trang (F5) và backend trả 401 → luôn redirect login
981
+ if (isPageReload()) {
982
+ const returnUrl = window.location.href;
983
+ authService.login(returnUrl);
984
+ return;
985
+ }
986
+ // Nếu chưa có user trong session (first load ẩn danh) và chưa đặt cờ hết hạn → redirect ngay đến login
987
+ if (!hasSessionUser() && !storage.isAuthExpired()) {
988
+ const returnUrl = window.location.href;
989
+ authService.login(returnUrl);
990
+ return;
991
+ }
992
+ if (isShowingDialog)
993
+ return;
994
+ isShowingDialog = true;
995
+ try {
996
+ const confirmed = await authDialog.openReLoginDialog();
997
+ if (confirmed) {
998
+ storage.setAuthExpired(false);
999
+ openReLogin();
1000
+ }
1001
+ else {
1002
+ storage.setAuthExpired(true);
1003
+ }
1004
+ }
1005
+ finally {
1006
+ isShowingDialog = false;
1007
+ }
1008
+ }
1009
+ function setHeaders() {
1010
+ let headers = new HttpHeaders();
1011
+ headers = headers.set("ConcungContextID", generateUUID().toString().replace(/-/g, ""));
1012
+ headers = headers.set("returnUrl", window.location.href);
1013
+ return headers;
1014
+ }
1015
+ function openReLogin() {
1016
+ var newwindow;
1017
+ let screenX = typeof window.screenX != "undefined" ? window.screenX : window.screenLeft;
1018
+ let screenY = typeof window.screenY != "undefined" ? window.screenY : window.screenTop;
1019
+ let outerWidth = typeof window.outerWidth != "undefined"
1020
+ ? window.outerWidth
1021
+ : document.body.clientWidth;
1022
+ let outerHeight = typeof window.outerHeight != "undefined"
1023
+ ? window.outerHeight
1024
+ : document.body.clientHeight - 22;
1025
+ let width = 550;
1026
+ let height = 600;
1027
+ let left = screenX + (outerWidth - width) / 2;
1028
+ let top = screenY + (outerHeight - height) / 3;
1029
+ let features = [
1030
+ `width=${width}`,
1031
+ `height=${height}`,
1032
+ `left=${left}`,
1033
+ `top=${top}`,
1034
+ "resizable=yes",
1035
+ "scrollbars=yes",
1036
+ "status=no",
1037
+ "location=no",
1038
+ "menubar=no",
1039
+ "toolbar=no"
1040
+ ].join(",");
1041
+ setTimeout(() => {
1042
+ var returnUrl = `${window.document.baseURI}` + "account/relogin";
1043
+ newwindow = window.open(`/accounts/relogin?returnUrl=${returnUrl}`, "Đăng nhập lại", features);
1044
+ if (window.focus && newwindow) {
1045
+ newwindow.focus();
1046
+ }
1047
+ }, 100);
1048
+ return false;
1049
+ }
1050
+ function generateUUID() {
1051
+ let dt = new Date().getTime();
1052
+ const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
1053
+ const r = (dt + Math.random() * 16) % 16 | 0;
1054
+ dt = Math.floor(dt / 16);
1055
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
1056
+ });
1057
+ return uuid.replace(/-/g, "");
1058
+ }
1059
+
1060
+ // Cache để lưu trữ pending requests
1061
+ const pendingRequests = new Map();
1062
+ const duplicateRequestInterceptor = (request, next) => {
1063
+ // Chỉ áp dụng cho GET requests
1064
+ if (request.method !== "GET") {
1065
+ return next(request);
1066
+ }
1067
+ const cacheKey = generateCacheKey(request);
1068
+ // Kiểm tra nếu có request đang pending
1069
+ if (pendingRequests.has(cacheKey)) {
1070
+ console.log(`[DuplicateRequestInterceptor] Duplicate request detected for: ${cacheKey}`);
1071
+ return pendingRequests.get(cacheKey);
1072
+ }
1073
+ // Tạo request mới
1074
+ const request$ = next(request).pipe(take(1), catchError$1(error => {
1075
+ // Xóa request khỏi pending khi có lỗi
1076
+ pendingRequests.delete(cacheKey);
1077
+ throw error;
1078
+ }), switchMap(response => {
1079
+ // Xóa request khỏi pending khi hoàn thành
1080
+ pendingRequests.delete(cacheKey);
1081
+ return of(response);
1082
+ }));
1083
+ // Lưu request vào pending
1084
+ pendingRequests.set(cacheKey, request$);
1085
+ return request$;
1086
+ };
1087
+ function generateCacheKey(request) {
1088
+ const url = request.url;
1089
+ const params = request.params.toString();
1090
+ return `${request.method}:${url}${params ? `?${params}` : ""}`;
1091
+ }
1092
+
1093
+ /**
1094
+ * Global Error Interceptor
1095
+ * Handles HTTP errors globally and shows appropriate user-friendly messages
1096
+ */
1097
+ const errorInterceptor = (req, next) => {
1098
+ const messageService = inject(MessageService);
1099
+ const router = inject(Router);
1100
+ return next(req).pipe(catchError((error) => {
1101
+ // Don't show error for auth interceptor handled errors
1102
+ if (error.status === HttpStatusCode.Unauthorized) {
1103
+ // Auth interceptor already handles this
1104
+ return throwError(() => error);
1105
+ }
1106
+ // Don't show error for cancelled requests
1107
+ if (error.status === 0 && error.error?.name === "TimeoutError") {
1108
+ return throwError(() => error);
1109
+ }
1110
+ // Handle different error statuses
1111
+ switch (error.status) {
1112
+ case HttpStatusCode.BadRequest:
1113
+ // messageService.add({
1114
+ // severity: "error",
1115
+ // summary: "Lỗi dữ liệu",
1116
+ // detail:
1117
+ // error.error?.message ||
1118
+ // "Dữ liệu không hợp lệ. Vui lòng kiểm tra lại.",
1119
+ // life: 3000
1120
+ // });
1121
+ break;
1122
+ case HttpStatusCode.Unauthorized:
1123
+ // Already handled by auth interceptor
1124
+ return throwError(() => error);
1125
+ case HttpStatusCode.Forbidden:
1126
+ // messageService.add({
1127
+ // severity: "error",
1128
+ // summary: "Không có quyền",
1129
+ // detail: "Bạn không có quyền thực hiện thao tác này.",
1130
+ // life: 3000
1131
+ // });
1132
+ router.navigate(["/notfound"]);
1133
+ break;
1134
+ case HttpStatusCode.NotFound:
1135
+ // messageService.add({
1136
+ // severity: "warn",
1137
+ // summary: "Không tìm thấy",
1138
+ // detail: error.error?.message || "Tài nguyên không tồn tại.",
1139
+ // life: 3000
1140
+ // });
1141
+ break;
1142
+ case HttpStatusCode.InternalServerError:
1143
+ // messageService.add({
1144
+ // severity: "error",
1145
+ // summary: "Lỗi server",
1146
+ // detail: "Server đang gặp sự cố. Vui lòng thử lại sau.",
1147
+ // life: 4000
1148
+ // });
1149
+ break;
1150
+ case HttpStatusCode.ServiceUnavailable:
1151
+ case HttpStatusCode.GatewayTimeout:
1152
+ // messageService.add({
1153
+ // severity: "error",
1154
+ // summary: "Dịch vụ không khả dụng",
1155
+ // detail: "Dịch vụ tạm thời không khả dụng. Vui lòng thử lại sau.",
1156
+ // life: 4000
1157
+ // });
1158
+ break;
1159
+ case HttpStatusCode.NotAcceptable:
1160
+ // 406 - Let component handle the error message
1161
+ // Component will show appropriate toast via effect
1162
+ break;
1163
+ case 0:
1164
+ // Network error or CORS error
1165
+ // messageService.add({
1166
+ // severity: "error",
1167
+ // summary: "Lỗi kết nối",
1168
+ // detail:
1169
+ // "Không thể kết nối đến server. Vui lòng kiểm tra kết nối mạng.",
1170
+ // life: 4000
1171
+ // });
1172
+ break;
1173
+ default:
1174
+ const errorMessage = error.error?.message || "Có lỗi xảy ra. Vui lòng thử lại.";
1175
+ messageService.add({
1176
+ severity: "error",
1177
+ summary: "Lỗi",
1178
+ detail: errorMessage,
1179
+ life: 3000
1180
+ });
1181
+ }
1182
+ return throwError(() => error);
1183
+ }));
1184
+ };
1185
+
1186
+ class LoadingService {
1187
+ constructor() {
1188
+ this.activeRequestsCount = 0;
1189
+ this.isLoading = signal(false);
1190
+ }
1191
+ start() {
1192
+ this.activeRequestsCount++;
1193
+ if (!this.isLoading())
1194
+ this.isLoading.set(true);
1195
+ }
1196
+ stop() {
1197
+ if (this.activeRequestsCount > 0)
1198
+ this.activeRequestsCount--;
1199
+ if (this.activeRequestsCount === 0 && this.isLoading()) {
1200
+ this.isLoading.set(false);
1201
+ }
1202
+ }
1203
+ reset() {
1204
+ this.activeRequestsCount = 0;
1205
+ this.isLoading.set(false);
1206
+ }
1207
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1208
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingService, providedIn: "root" }); }
1209
+ }
1210
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingService, decorators: [{
1211
+ type: Injectable,
1212
+ args: [{ providedIn: "root" }]
1213
+ }] });
1214
+
1215
+ const loadingInterceptor = (req, next) => {
1216
+ const loadingService = inject(LoadingService);
1217
+ // Skip loading for specific APIs that are called frequently
1218
+ const skipLoadingUrls = [
1219
+ // Add more URLs here if needed
1220
+ ];
1221
+ const shouldSkipLoading = skipLoadingUrls.some(url => req.url.includes(url));
1222
+ if (shouldSkipLoading) {
1223
+ return next(req);
1224
+ }
1225
+ loadingService.start();
1226
+ return next(req).pipe(finalize(() => loadingService.stop()));
1227
+ };
1228
+
1229
+ // Injection token for environment (optional - app can provide it)
1230
+ const APP_ENVIRONMENT = Symbol('APP_ENVIRONMENT');
1231
+ class AppConfigService {
1232
+ constructor(http, env) {
1233
+ this.http = http;
1234
+ this.configSubject = new BehaviorSubject(null);
1235
+ this.config = null;
1236
+ this.environment = null;
1237
+ // Try to get environment from injection, or from window (for backward compatibility)
1238
+ this.environment = env || (typeof window !== 'undefined' && window.__ENVIRONMENT__) || null;
1239
+ }
1240
+ /**
1241
+ * Lấy config từ backend và lưu vào service, fallback về environment nếu fail
1242
+ */
1243
+ loadConfig() {
1244
+ return this.http.post("/api1/configs", {}).pipe(map$1(response => response.data), tap$1(config => {
1245
+ this.config = config;
1246
+ this.configSubject.next(config);
1247
+ }), catchError$1(error => {
1248
+ console.warn("⚠️ Failed to load config from backend, using fallback from environment:", error);
1249
+ return this.loadFallbackConfig();
1250
+ }));
1251
+ }
1252
+ /**
1253
+ * Load fallback config từ environment
1254
+ */
1255
+ loadFallbackConfig() {
1256
+ const env = this.environment?.appConfig;
1257
+ const fallbackConfig = {
1258
+ Environment: env?.Environment || "",
1259
+ ClientID: env?.ClientID || ""
1260
+ };
1261
+ this.config = fallbackConfig;
1262
+ this.configSubject.next(fallbackConfig);
1263
+ return of(fallbackConfig);
1264
+ }
1265
+ /**
1266
+ * Lấy config hiện tại (observable)
1267
+ */
1268
+ getConfig() {
1269
+ return this.configSubject.asObservable();
1270
+ }
1271
+ /**
1272
+ * Lấy config hiện tại (sync)
1273
+ */
1274
+ getConfigSync() {
1275
+ return this.config;
1276
+ }
1277
+ /**
1278
+ * Lấy Environment
1279
+ */
1280
+ getEnvironment() {
1281
+ const env = this.environment?.appConfig;
1282
+ return this.config?.Environment || env?.Environment || "";
1283
+ }
1284
+ /**
1285
+ * Lấy ClientID/ClientName
1286
+ */
1287
+ getClientName() {
1288
+ const env = this.environment?.appConfig;
1289
+ return this.config?.ClientID || env?.ClientID || "";
1290
+ }
1291
+ /**
1292
+ * Check xem đã load config chưa
1293
+ */
1294
+ isConfigLoaded() {
1295
+ return this.config !== null;
1296
+ }
1297
+ /**
1298
+ * Check xem config có được load từ backend hay fallback
1299
+ */
1300
+ isLoadedFromBackend() {
1301
+ // Compare với environment để check xem có được load từ backend không
1302
+ const env = this.environment?.appConfig;
1303
+ return (this.config !== null &&
1304
+ env &&
1305
+ this.config.Environment !== env.Environment);
1306
+ }
1307
+ /**
1308
+ * Lấy URL để clear cache khi tạo mới landing page
1309
+ */
1310
+ getCacheClearUrlForCreate() {
1311
+ return this.environment?.landingPageConfig?.cacheClearUrlForCreate || null;
1312
+ }
1313
+ /**
1314
+ * Lấy URL để clear cache khi cập nhật landing page
1315
+ */
1316
+ getCacheClearUrlForUpdate() {
1317
+ return this.environment?.landingPageConfig?.cacheClearUrlForUpdate || null;
1318
+ }
1319
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppConfigService, deps: [{ token: i1$2.HttpClient }, { token: APP_ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
1320
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppConfigService, providedIn: "root" }); }
1321
+ }
1322
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppConfigService, decorators: [{
1323
+ type: Injectable,
1324
+ args: [{
1325
+ providedIn: "root"
1326
+ }]
1327
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: undefined, decorators: [{
1328
+ type: Optional
1329
+ }, {
1330
+ type: Inject,
1331
+ args: [APP_ENVIRONMENT]
1332
+ }] }] });
1333
+
1334
+ class AppInitializeService {
1335
+ constructor() {
1336
+ this.initAppSubject = new BehaviorSubject(false);
1337
+ this.appConfigService = inject(AppConfigService);
1338
+ }
1339
+ initialize() {
1340
+ return this.initAppSubject.asObservable();
1341
+ }
1342
+ /**
1343
+ * Khởi tạo app: Load config từ backend hoặc fallback từ environment
1344
+ */
1345
+ initializeApp() {
1346
+ return this.appConfigService.loadConfig().pipe(map$1(() => {
1347
+ this.setInitialize(true);
1348
+ const configSource = this.appConfigService.isLoadedFromBackend()
1349
+ ? "backend"
1350
+ : "environment fallback";
1351
+ return true;
1352
+ })
1353
+ // Không cần catchError nữa vì service đã handle fallback internally
1354
+ );
1355
+ }
1356
+ setInitialize(data) {
1357
+ this.initAppSubject.next(data);
1358
+ }
1359
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppInitializeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1360
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppInitializeService, providedIn: "root" }); }
1361
+ }
1362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppInitializeService, decorators: [{
1363
+ type: Injectable,
1364
+ args: [{ providedIn: "root" }]
1365
+ }] });
1366
+
1367
+ class CookieService {
1368
+ constructor(cookieService) {
1369
+ this.cookieService = cookieService;
1370
+ }
1371
+ set(name, value, days = 1) {
1372
+ this.cookieService.set(name, value, days);
1373
+ }
1374
+ get(name) {
1375
+ return this.cookieService.get(name);
1376
+ }
1377
+ delete(name) {
1378
+ this.cookieService.delete(name);
1379
+ }
1380
+ check(name) {
1381
+ return this.cookieService.check(name);
1382
+ }
1383
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CookieService, deps: [{ token: i1$3.CookieService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1384
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CookieService, providedIn: "root" }); }
1385
+ }
1386
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CookieService, decorators: [{
1387
+ type: Injectable,
1388
+ args: [{ providedIn: "root" }]
1389
+ }], ctorParameters: () => [{ type: i1$3.CookieService }] });
1390
+
1391
+ class DocumentService {
1392
+ constructor(platformService) {
1393
+ this.platformService = platformService;
1394
+ }
1395
+ get nativeDocument() {
1396
+ return this.platformService.getDocument();
1397
+ }
1398
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DocumentService, deps: [{ token: PlatformService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1399
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DocumentService, providedIn: "root" }); }
1400
+ }
1401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DocumentService, decorators: [{
1402
+ type: Injectable,
1403
+ args: [{ providedIn: "root" }]
1404
+ }], ctorParameters: () => [{ type: PlatformService }] });
1405
+
1406
+ class LoaderService {
1407
+ constructor() {
1408
+ this._loading = new BehaviorSubject(false);
1409
+ this.loading$ = this._loading.asObservable();
1410
+ }
1411
+ show() {
1412
+ this._loading.next(true);
1413
+ }
1414
+ hide() {
1415
+ this._loading.next(false);
1416
+ }
1417
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1418
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoaderService, providedIn: "root" }); }
1419
+ }
1420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoaderService, decorators: [{
1421
+ type: Injectable,
1422
+ args: [{
1423
+ providedIn: "root"
1424
+ }]
1425
+ }] });
1426
+
1427
+ class FileService {
1428
+ constructor(_http, _loader) {
1429
+ this._http = _http;
1430
+ this._loader = _loader;
1431
+ this.baseUrl = "/api1/files/filehandler";
1432
+ }
1433
+ uploadExcel(file) {
1434
+ const formData = new FormData();
1435
+ formData.append("file", file);
1436
+ const req = new HttpRequest("POST", `${this.baseUrl}/uploadExcel`, formData, {
1437
+ reportProgress: true,
1438
+ responseType: "json"
1439
+ });
1440
+ return this._http.request(req);
1441
+ }
1442
+ downloadExcel(filePathOrRes, fileName = "") {
1443
+ this._loader.show();
1444
+ // Support passing raw path or API response { data: path } or { file_path: path }
1445
+ let filePath = undefined;
1446
+ if (typeof filePathOrRes === "string") {
1447
+ filePath = filePathOrRes;
1448
+ }
1449
+ else if (filePathOrRes) {
1450
+ filePath =
1451
+ filePathOrRes.data || filePathOrRes.file_path || filePathOrRes.FilePath;
1452
+ }
1453
+ if (!filePath) {
1454
+ this._loader.hide();
1455
+ return;
1456
+ }
1457
+ if (!fileName) {
1458
+ const timestamp = Date.now();
1459
+ fileName = `file_${timestamp}.xlsx`;
1460
+ }
1461
+ const encodedPath = encodeURIComponent(filePath);
1462
+ this._http
1463
+ .get(`${this.baseUrl}/downloadExcel?filePath=${encodedPath}`, {
1464
+ responseType: "blob"
1465
+ })
1466
+ .subscribe({
1467
+ next: response => {
1468
+ if (response) {
1469
+ let blob = new Blob([response]);
1470
+ this.createElementDownload(blob, fileName);
1471
+ }
1472
+ this._loader.hide();
1473
+ },
1474
+ error: () => {
1475
+ this._loader.hide();
1476
+ }
1477
+ });
1478
+ }
1479
+ exportExcel(sheet_datas, fileName) {
1480
+ this._loader.show();
1481
+ this.exportExcelBase(sheet_datas).subscribe(event => {
1482
+ if (event.body) {
1483
+ let blob = new Blob([event.body]);
1484
+ this.createElementDownload(blob, fileName);
1485
+ this._loader.hide();
1486
+ }
1487
+ });
1488
+ }
1489
+ exportExcelBase(sheet_datas) {
1490
+ const formData = new FormData();
1491
+ formData.append("sheet_datas", JSON.stringify(sheet_datas));
1492
+ const req = new HttpRequest("POST", `${this.baseUrl}/exportExcel`, formData, { responseType: "blob" });
1493
+ return this._http.request(req);
1494
+ }
1495
+ createElementDownload(blob, fileName = "") {
1496
+ const parts = fileName.split(".");
1497
+ let extention = parts.pop();
1498
+ let name = fileName.replace(`.${extention}`, "");
1499
+ let downLoadFileName = `${name}_${moment(new Date()).format("DD-MM-YYYY")}_.${extention ? extention : ".xlsx"}`;
1500
+ let downloadLink = document.createElement("a");
1501
+ downloadLink.href = window.URL.createObjectURL(blob);
1502
+ downloadLink.setAttribute("download", downLoadFileName);
1503
+ document.body.appendChild(downloadLink);
1504
+ downloadLink.click();
1505
+ setTimeout(() => {
1506
+ document.body.removeChild(downloadLink);
1507
+ }, 100);
1508
+ }
1509
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileService, deps: [{ token: i1$2.HttpClient }, { token: LoaderService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1510
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileService, providedIn: "root" }); }
1511
+ }
1512
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileService, decorators: [{
1513
+ type: Injectable,
1514
+ args: [{ providedIn: "root" }]
1515
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: LoaderService }] });
1516
+
1517
+ class FileGCSService {
1518
+ constructor(_http, _apiService) {
1519
+ this._http = _http;
1520
+ this._apiService = _apiService;
1521
+ this.baseUrl = "/api1/files/gcs";
1522
+ }
1523
+ getSignedLink(filePath) {
1524
+ return this._apiService.post(`${this.baseUrl}/getSignedLink?filePath=${filePath}`);
1525
+ }
1526
+ attachFile(file) {
1527
+ const formData = new FormData();
1528
+ formData.append("file", file);
1529
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1530
+ reportProgress: true,
1531
+ responseType: "json"
1532
+ });
1533
+ return this._http.request(req);
1534
+ }
1535
+ attachFilePromise(file) {
1536
+ const formData = new FormData();
1537
+ formData.append("file", file);
1538
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1539
+ reportProgress: true,
1540
+ responseType: "json"
1541
+ });
1542
+ return firstValueFrom(this._http.request(req));
1543
+ }
1544
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileGCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1545
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileGCSService, providedIn: "root" }); }
1546
+ }
1547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileGCSService, decorators: [{
1548
+ type: Injectable,
1549
+ args: [{ providedIn: "root" }]
1550
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ApiService }] });
1551
+
1552
+ class FileAIA_GCSService {
1553
+ constructor(_http, _apiService) {
1554
+ this._http = _http;
1555
+ this._apiService = _apiService;
1556
+ this.baseUrl = "/api1/files/aia-gcs";
1557
+ }
1558
+ getSignedLink(filePath) {
1559
+ return this._apiService.post(`${this.baseUrl}/getSignedLink?filePath=${filePath}`);
1560
+ }
1561
+ attachFile(file) {
1562
+ const formData = new FormData();
1563
+ formData.append("file", file);
1564
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1565
+ reportProgress: true,
1566
+ responseType: "json"
1567
+ });
1568
+ return this._http.request(req);
1569
+ }
1570
+ attachFilePromise(file) {
1571
+ const formData = new FormData();
1572
+ formData.append("file", file);
1573
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1574
+ reportProgress: true,
1575
+ responseType: "json"
1576
+ });
1577
+ return firstValueFrom(this._http.request(req));
1578
+ }
1579
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileAIA_GCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1580
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileAIA_GCSService, providedIn: "root" }); }
1581
+ }
1582
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileAIA_GCSService, decorators: [{
1583
+ type: Injectable,
1584
+ args: [{ providedIn: "root" }]
1585
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ApiService }] });
1586
+
1587
+ class FileWAPGCSService {
1588
+ constructor(_http, _apiService) {
1589
+ this._http = _http;
1590
+ this._apiService = _apiService;
1591
+ this.baseUrl = "/api1/files/wap-gcs";
1592
+ }
1593
+ getSignedLink(filePath) {
1594
+ return this._apiService.post(`${this.baseUrl}/getSignedLink?filePath=${filePath}`);
1595
+ }
1596
+ getPresignedLink(fileName) {
1597
+ return this._apiService.post(`${this.baseUrl}/getPresignedLink?fileName=${fileName}`);
1598
+ }
1599
+ setPublic(filePath) {
1600
+ return this._apiService.post(`${this.baseUrl}/setPublic?filePath=${filePath}`);
1601
+ }
1602
+ attachFile(file) {
1603
+ const formData = new FormData();
1604
+ formData.append("file", file);
1605
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1606
+ reportProgress: true,
1607
+ responseType: "json"
1608
+ });
1609
+ return this._http.request(req);
1610
+ }
1611
+ attachFilePromise(file) {
1612
+ const formData = new FormData();
1613
+ formData.append("file", file);
1614
+ const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1615
+ reportProgress: true,
1616
+ responseType: "json"
1617
+ });
1618
+ return firstValueFrom(this._http.request(req));
1619
+ }
1620
+ uploadFile(file) {
1621
+ return new Observable(observer => {
1622
+ let xhr = null;
1623
+ // Tạo tên file unique để tránh trùng tên khi upload nhiều lần
1624
+ const uniqueFileName = this.generateUniqueFileName(file.name);
1625
+ firstValueFrom(this.getPresignedLink(uniqueFileName))
1626
+ .then(presignedData => {
1627
+ if (!presignedData) {
1628
+ throw new Error("Không nhận được presigned link từ server");
1629
+ }
1630
+ const presignedURL = presignedData.PreSignedURL ||
1631
+ presignedData.preSignedURL ||
1632
+ presignedData.url;
1633
+ const filePath = presignedData.FilePath || presignedData.filePath;
1634
+ const absoluteURL = presignedData.AbsoluteURL || presignedData.absoluteURL;
1635
+ if (!presignedURL) {
1636
+ throw new Error("Presigned URL không hợp lệ");
1637
+ }
1638
+ const contentType = file.type || "application/octet-stream";
1639
+ xhr = new XMLHttpRequest();
1640
+ const cleanup = () => {
1641
+ if (xhr) {
1642
+ xhr.upload.removeEventListener("progress", progressHandler);
1643
+ xhr.removeEventListener("load", loadHandler);
1644
+ xhr.removeEventListener("error", errorHandler);
1645
+ xhr.removeEventListener("timeout", timeoutHandler);
1646
+ xhr.abort();
1647
+ xhr = null;
1648
+ }
1649
+ };
1650
+ const progressHandler = (_event) => {
1651
+ // No-op: progress not consumed by current callers
1652
+ };
1653
+ const loadHandler = async () => {
1654
+ if (xhr && xhr.status === 200) {
1655
+ try {
1656
+ if (filePath) {
1657
+ const setPublicResult = await firstValueFrom(this.setPublic(filePath));
1658
+ if (setPublicResult !== true) {
1659
+ throw new Error("Set public không thành công");
1660
+ }
1661
+ }
1662
+ const finalUrl = absoluteURL || presignedURL;
1663
+ const result = this.buildResult(file, filePath, finalUrl);
1664
+ cleanup();
1665
+ observer.next(result);
1666
+ observer.complete();
1667
+ }
1668
+ catch (err) {
1669
+ cleanup();
1670
+ const msg = err?.message ||
1671
+ "Upload thành công nhưng setPublic thất bại. Vui lòng thử lại.";
1672
+ observer.error(new Error(msg));
1673
+ }
1674
+ }
1675
+ else if (xhr) {
1676
+ const errorMessage = `Upload file thất bại: ${xhr.statusText} (${xhr.status})`;
1677
+ cleanup();
1678
+ observer.error(new Error(errorMessage));
1679
+ }
1680
+ };
1681
+ const errorHandler = () => {
1682
+ if (!xhr)
1683
+ return;
1684
+ let userMessage = "Upload file thất bại";
1685
+ if (xhr.status === 0) {
1686
+ userMessage =
1687
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
1688
+ }
1689
+ else if (xhr.status >= 500) {
1690
+ userMessage = "Lỗi máy chủ. Vui lòng thử lại sau.";
1691
+ }
1692
+ else if (xhr.status >= 400) {
1693
+ userMessage = `Lỗi yêu cầu: ${xhr.statusText}`;
1694
+ }
1695
+ cleanup();
1696
+ observer.error(new Error(userMessage));
1697
+ };
1698
+ const timeoutHandler = () => {
1699
+ cleanup();
1700
+ observer.error(new Error("Kết nối đến máy chủ hết thời gian. Vui lòng thử lại."));
1701
+ };
1702
+ xhr.upload.addEventListener("progress", progressHandler);
1703
+ xhr.addEventListener("load", loadHandler);
1704
+ xhr.addEventListener("error", errorHandler);
1705
+ xhr.addEventListener("timeout", timeoutHandler);
1706
+ xhr.timeout = 60000 * 5;
1707
+ xhr.open("PUT", presignedURL, true);
1708
+ xhr.setRequestHeader("Content-Type", contentType);
1709
+ xhr.send(file);
1710
+ })
1711
+ .catch(error => {
1712
+ if (xhr) {
1713
+ xhr.abort();
1714
+ xhr = null;
1715
+ }
1716
+ let userMessage = "Upload file thất bại";
1717
+ if (error.errorMessage) {
1718
+ userMessage = error.errorMessage;
1719
+ }
1720
+ else if (error.code === "ECONNABORTED" ||
1721
+ error.message?.includes("timeout")) {
1722
+ userMessage =
1723
+ "Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.";
1724
+ }
1725
+ else if (error.message?.includes("failed to respond") ||
1726
+ error.message?.includes("Network Error")) {
1727
+ userMessage =
1728
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
1729
+ }
1730
+ else if (error.message) {
1731
+ userMessage = `Lỗi kết nối: ${error.message}`;
1732
+ }
1733
+ else if (error.error?.message) {
1734
+ userMessage = error.error.message;
1735
+ }
1736
+ observer.error(new Error(userMessage));
1737
+ });
1738
+ return () => {
1739
+ if (xhr) {
1740
+ xhr.abort();
1741
+ xhr = null;
1742
+ }
1743
+ };
1744
+ });
1745
+ }
1746
+ /**
1747
+ * Tạo tên file unique bằng cách thêm timestamp và random string
1748
+ * Format: originalname-timestamp-random.ext
1749
+ */
1750
+ generateUniqueFileName(originalFileName) {
1751
+ const timestamp = Date.now();
1752
+ const randomString = Math.random().toString(36).substring(2, 9);
1753
+ // Tách tên file và extension
1754
+ const lastDotIndex = originalFileName.lastIndexOf(".");
1755
+ if (lastDotIndex === -1) {
1756
+ // Không có extension
1757
+ return `${originalFileName}-${timestamp}-${randomString}`;
1758
+ }
1759
+ const nameWithoutExt = originalFileName.substring(0, lastDotIndex);
1760
+ const extension = originalFileName.substring(lastDotIndex);
1761
+ return `${nameWithoutExt}-${timestamp}-${randomString}${extension}`;
1762
+ }
1763
+ extractFileName(filePath) {
1764
+ return filePath.split("/").pop() || "";
1765
+ }
1766
+ buildResult(file, filePath, url) {
1767
+ return {
1768
+ url,
1769
+ filePath,
1770
+ fileName: this.extractFileName(filePath) || file.name,
1771
+ originalName: file.name
1772
+ };
1773
+ }
1774
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileWAPGCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1775
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileWAPGCSService, providedIn: "root" }); }
1776
+ }
1777
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileWAPGCSService, decorators: [{
1778
+ type: Injectable,
1779
+ args: [{ providedIn: "root" }]
1780
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ApiService }] });
1781
+
1782
+ class LoadingSpinnerService {
1783
+ constructor() {
1784
+ this._loading = new BehaviorSubject(false);
1785
+ this.loading$ = this._loading.asObservable();
1786
+ }
1787
+ show() {
1788
+ this._loading.next(true);
1789
+ }
1790
+ hide() {
1791
+ this._loading.next(false);
1792
+ }
1793
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingSpinnerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1794
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingSpinnerService, providedIn: "root" }); }
1795
+ }
1796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoadingSpinnerService, decorators: [{
1797
+ type: Injectable,
1798
+ args: [{
1799
+ providedIn: "root"
1800
+ }]
1801
+ }] });
1802
+
1803
+ class LocalStorageService {
1804
+ constructor(platformService) {
1805
+ this.platformService = platformService;
1806
+ }
1807
+ setItem(key, value) {
1808
+ this.platformService.runOnBrowser(() => {
1809
+ localStorage.setItem(key, value);
1810
+ });
1811
+ }
1812
+ getItem(key) {
1813
+ return this.platformService.runOnBrowser(() => {
1814
+ return localStorage.getItem(key);
1815
+ });
1816
+ }
1817
+ removeItem(key) {
1818
+ this.platformService.runOnBrowser(() => {
1819
+ localStorage.removeItem(key);
1820
+ });
1821
+ }
1822
+ clear() {
1823
+ this.platformService.runOnBrowser(() => {
1824
+ localStorage.clear();
1825
+ });
1826
+ }
1827
+ hasKey(key) {
1828
+ return (this.platformService.runOnBrowser(() => {
1829
+ return localStorage.getItem(key) !== null;
1830
+ }) || false);
1831
+ }
1832
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LocalStorageService, deps: [{ token: PlatformService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1833
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LocalStorageService, providedIn: "root" }); }
1834
+ }
1835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LocalStorageService, decorators: [{
1836
+ type: Injectable,
1837
+ args: [{
1838
+ providedIn: "root"
1839
+ }]
1840
+ }], ctorParameters: () => [{ type: PlatformService }] });
1841
+
1842
+ class OverlayService {
1843
+ constructor(_document) {
1844
+ this._document = _document;
1845
+ this.isOverlaySubject = new BehaviorSubject(false);
1846
+ this.isOverlay$ = this.isOverlaySubject.asObservable();
1847
+ this.overlayRequestors = new Set(); // Set lưu các component yêu cầu overlay
1848
+ this.htmlWrapperElement = this._document.querySelector("html");
1849
+ }
1850
+ onShowOverlay(requestor) {
1851
+ this.overlayRequestors.add(requestor); // Thêm component vào Set
1852
+ if (this.overlayRequestors.size === 1) {
1853
+ // Chỉ hiển thị overlay nếu là yêu cầu đầu tiên
1854
+ this.isOverlaySubject.next(true);
1855
+ this.htmlWrapperElement.style.overflow = "hidden";
1856
+ }
1857
+ }
1858
+ onHiddenOverlay(requestor) {
1859
+ this.overlayRequestors.delete(requestor); // Xóa component khỏi Set
1860
+ if (this.overlayRequestors.size === 0) {
1861
+ // Chỉ ẩn overlay nếu không còn yêu cầu nào
1862
+ this.isOverlaySubject.next(false);
1863
+ this.htmlWrapperElement.style.overflow = "auto";
1864
+ }
1865
+ }
1866
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayService, deps: [{ token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
1867
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayService, providedIn: "root" }); }
1868
+ }
1869
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayService, decorators: [{
1870
+ type: Injectable,
1871
+ args: [{
1872
+ providedIn: "root"
1873
+ }]
1874
+ }], ctorParameters: () => [{ type: Document, decorators: [{
1875
+ type: Inject,
1876
+ args: [DOCUMENT]
1877
+ }] }] });
1878
+
1879
+ class RequestCacheService {
1880
+ constructor() {
1881
+ this.cache = new Map();
1882
+ this.pendingRequests = new Map();
1883
+ this.cacheStats = new BehaviorSubject({
1884
+ hits: 0,
1885
+ misses: 0
1886
+ });
1887
+ // Cleanup expired cache entries every 5 minutes
1888
+ setInterval(() => this.cleanupExpiredEntries(), 5 * 60 * 1000);
1889
+ }
1890
+ /**
1891
+ * Lấy dữ liệu từ cache hoặc thực hiện request mới
1892
+ */
1893
+ get(key, requestFn, ttl = 5 * 60 * 1000) {
1894
+ // Kiểm tra cache
1895
+ const cached = this.getFromCache(key);
1896
+ if (cached) {
1897
+ this.incrementHits();
1898
+ return of(cached);
1899
+ }
1900
+ // Kiểm tra pending requests
1901
+ if (this.pendingRequests.has(key)) {
1902
+ return this.pendingRequests.get(key);
1903
+ }
1904
+ // Thực hiện request mới
1905
+ this.incrementMisses();
1906
+ const request$ = requestFn().pipe(tap$1(data => {
1907
+ this.setCache(key, data, ttl);
1908
+ this.pendingRequests.delete(key);
1909
+ }), catchError$1(error => {
1910
+ this.pendingRequests.delete(key);
1911
+ throw error;
1912
+ }));
1913
+ this.pendingRequests.set(key, request$);
1914
+ return request$;
1915
+ }
1916
+ /**
1917
+ * Lấy dữ liệu từ cache
1918
+ */
1919
+ getFromCache(key) {
1920
+ const entry = this.cache.get(key);
1921
+ if (!entry)
1922
+ return null;
1923
+ const now = Date.now();
1924
+ if (now - entry.timestamp > entry.ttl) {
1925
+ this.cache.delete(key);
1926
+ return null;
1927
+ }
1928
+ return entry.data;
1929
+ }
1930
+ /**
1931
+ * Lưu dữ liệu vào cache
1932
+ */
1933
+ setCache(key, data, ttl) {
1934
+ this.cache.set(key, {
1935
+ data,
1936
+ timestamp: Date.now(),
1937
+ ttl
1938
+ });
1939
+ }
1940
+ /**
1941
+ * Xóa cache entry
1942
+ */
1943
+ clearCache(key) {
1944
+ this.cache.delete(key);
1945
+ this.pendingRequests.delete(key);
1946
+ }
1947
+ /**
1948
+ * Xóa toàn bộ cache
1949
+ */
1950
+ clearAllCache() {
1951
+ this.cache.clear();
1952
+ this.pendingRequests.clear();
1953
+ }
1954
+ /**
1955
+ * Dọn dẹp cache entries đã hết hạn
1956
+ */
1957
+ cleanupExpiredEntries() {
1958
+ const now = Date.now();
1959
+ for (const [key, entry] of this.cache.entries()) {
1960
+ if (now - entry.timestamp > entry.ttl) {
1961
+ this.cache.delete(key);
1962
+ }
1963
+ }
1964
+ }
1965
+ /**
1966
+ * Tạo cache key từ URL và parameters
1967
+ */
1968
+ generateKey(url, params) {
1969
+ const paramsStr = params ? JSON.stringify(params) : "";
1970
+ return `${url}${paramsStr}`;
1971
+ }
1972
+ /**
1973
+ * Lấy thống kê cache
1974
+ */
1975
+ getStats() {
1976
+ return this.cacheStats.asObservable();
1977
+ }
1978
+ incrementHits() {
1979
+ const stats = this.cacheStats.value;
1980
+ this.cacheStats.next({ ...stats, hits: stats.hits + 1 });
1981
+ }
1982
+ incrementMisses() {
1983
+ const stats = this.cacheStats.value;
1984
+ this.cacheStats.next({ ...stats, misses: stats.misses + 1 });
1985
+ }
1986
+ /**
1987
+ * Log cache statistics
1988
+ */
1989
+ logStats() {
1990
+ const stats = this.cacheStats.value;
1991
+ const total = stats.hits + stats.misses;
1992
+ const hitRate = total > 0 ? ((stats.hits / total) * 100).toFixed(2) : "0";
1993
+ // console.log(`[RequestCacheService] Cache Stats:`, {
1994
+ // hits: stats.hits,
1995
+ // misses: stats.misses,
1996
+ // total,
1997
+ // hitRate: `${hitRate}%`,
1998
+ // cacheSize: this.cache.size,
1999
+ // pendingRequests: this.pendingRequests.size
2000
+ // });
2001
+ }
2002
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RequestCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2003
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RequestCacheService, providedIn: "root" }); }
2004
+ }
2005
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RequestCacheService, decorators: [{
2006
+ type: Injectable,
2007
+ args: [{
2008
+ providedIn: "root"
2009
+ }]
2010
+ }], ctorParameters: () => [] });
2011
+
2012
+ class ResponsiveService {
2013
+ constructor(breakpointService) {
2014
+ this.breakpointService = breakpointService;
2015
+ this.currentView = new BehaviorSubject(ViewType.DESKTOP);
2016
+ this.currentView$ = this.currentView.asObservable();
2017
+ this.breakpointService.breakpointsResult$.subscribe(breakpoints => {
2018
+ if (breakpoints.isDesktopUp) {
2019
+ this.currentView.next(ViewType.DESKTOP);
2020
+ }
2021
+ else if (breakpoints.isPhoneOnly) {
2022
+ this.currentView.next(ViewType.MOBILE);
2023
+ }
2024
+ else {
2025
+ this.currentView.next(ViewType.DESKTOP); // Default to desktop
2026
+ }
2027
+ });
2028
+ }
2029
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResponsiveService, deps: [{ token: BreakpointService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2030
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResponsiveService, providedIn: "root" }); }
2031
+ }
2032
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResponsiveService, decorators: [{
2033
+ type: Injectable,
2034
+ args: [{
2035
+ providedIn: "root"
2036
+ }]
2037
+ }], ctorParameters: () => [{ type: BreakpointService }] });
2038
+
2039
+ /**
2040
+ * Search Event Types for Shell-MFE Communication
2041
+ * These mirror the types from the Search MFE but are simplified for cross-app communication
2042
+ */
2043
+ var ShellSearchEventType;
2044
+ (function (ShellSearchEventType) {
2045
+ ShellSearchEventType["NAVIGATION_TO_SEARCH"] = "NAVIGATION_TO_SEARCH";
2046
+ ShellSearchEventType["NAVIGATION_AWAY_FROM_SEARCH"] = "NAVIGATION_AWAY_FROM_SEARCH";
2047
+ ShellSearchEventType["NAVIGATION_REQUEST"] = "NAVIGATION_REQUEST";
2048
+ ShellSearchEventType["ROUTE_ACCESSED_DIRECTLY"] = "ROUTE_ACCESSED_DIRECTLY";
2049
+ ShellSearchEventType["ROUTE_RELOADED"] = "ROUTE_RELOADED";
2050
+ ShellSearchEventType["SEARCH_INITIATED"] = "SEARCH_INITIATED";
2051
+ ShellSearchEventType["FILTER_CHANGED"] = "FILTER_CHANGED";
2052
+ ShellSearchEventType["SORT_CHANGED"] = "SORT_CHANGED";
2053
+ })(ShellSearchEventType || (ShellSearchEventType = {}));
2054
+ /**
2055
+ * Search Event Bridge Service for Shell Application
2056
+ * Handles communication between Shell and Search MFE via postMessage and routing
2057
+ */
2058
+ class SearchEventBridgeService {
2059
+ constructor(router) {
2060
+ this.router = router;
2061
+ this.destroy$ = new Subject();
2062
+ this.eventSubject = new BehaviorSubject(null);
2063
+ // Current route tracking
2064
+ this.currentRoute = "";
2065
+ this.previousRoute = "";
2066
+ this.isSearchRoute = false;
2067
+ this.initializeRouteTracking();
2068
+ this.initializePostMessageListener();
2069
+ }
2070
+ /**
2071
+ * Get observable stream of search events
2072
+ */
2073
+ getEventStream() {
2074
+ return this.eventSubject.pipe(filter(event => event !== null), takeUntil(this.destroy$));
2075
+ }
2076
+ /**
2077
+ * Emit a search event from Shell
2078
+ */
2079
+ emitShellEvent(type, data) {
2080
+ const event = {
2081
+ type,
2082
+ source: "shell",
2083
+ timestamp: Date.now(),
2084
+ route: this.currentRoute,
2085
+ queryParams: this.getQueryParams(),
2086
+ data,
2087
+ correlationId: this.generateCorrelationId()
2088
+ };
2089
+ this.eventSubject.next(event);
2090
+ this.postMessageToMFE(event);
2091
+ }
2092
+ /**
2093
+ * Navigate to search page with query
2094
+ */
2095
+ navigateToSearch(searchQuery, additionalParams) {
2096
+ const queryParams = {
2097
+ q: searchQuery,
2098
+ ...additionalParams
2099
+ };
2100
+ // Emit navigation event
2101
+ this.emitShellEvent(ShellSearchEventType.NAVIGATION_TO_SEARCH, {
2102
+ searchQuery,
2103
+ queryParams
2104
+ });
2105
+ // Perform navigation
2106
+ this.router.navigate(["/tim-kiem"], { queryParams });
2107
+ }
2108
+ /**
2109
+ * Update search query in current route
2110
+ */
2111
+ updateSearchQuery(searchQuery, replaceUrl = false) {
2112
+ const currentParams = this.getQueryParams();
2113
+ const newParams = {
2114
+ ...currentParams,
2115
+ q: searchQuery
2116
+ };
2117
+ this.router.navigate([], {
2118
+ relativeTo: this.router.routerState.root,
2119
+ queryParams: newParams,
2120
+ replaceUrl
2121
+ });
2122
+ }
2123
+ /**
2124
+ * Clear search and navigate to home
2125
+ */
2126
+ clearSearchAndNavigateHome() {
2127
+ this.emitShellEvent(ShellSearchEventType.NAVIGATION_AWAY_FROM_SEARCH, {
2128
+ targetRoute: "/"
2129
+ });
2130
+ this.router.navigate(["/"]);
2131
+ }
2132
+ /**
2133
+ * Check if currently on search route
2134
+ */
2135
+ isOnSearchRoute() {
2136
+ return this.isSearchRoute;
2137
+ }
2138
+ /**
2139
+ * Get current route info
2140
+ */
2141
+ getCurrentRouteInfo() {
2142
+ return {
2143
+ route: this.currentRoute,
2144
+ queryParams: this.getQueryParams(),
2145
+ isSearchRoute: this.isSearchRoute
2146
+ };
2147
+ }
2148
+ /**
2149
+ * Initialize route tracking
2150
+ */
2151
+ initializeRouteTracking() {
2152
+ this.router.events
2153
+ .pipe(filter(event => event instanceof NavigationEnd), takeUntil(this.destroy$))
2154
+ .subscribe((event) => {
2155
+ this.handleRouteChange(event);
2156
+ });
2157
+ // Handle initial route
2158
+ this.currentRoute = this.router.url;
2159
+ this.isSearchRoute = this.checkIfSearchRoute(this.currentRoute);
2160
+ }
2161
+ /**
2162
+ * Handle route changes
2163
+ */
2164
+ handleRouteChange(event) {
2165
+ this.previousRoute = this.currentRoute;
2166
+ this.currentRoute = event.url;
2167
+ const wasSearchRoute = this.isSearchRoute;
2168
+ this.isSearchRoute = this.checkIfSearchRoute(this.currentRoute);
2169
+ // Emit appropriate events based on route change
2170
+ if (!wasSearchRoute && this.isSearchRoute) {
2171
+ // Navigated to search
2172
+ this.emitShellEvent(ShellSearchEventType.NAVIGATION_TO_SEARCH, {
2173
+ fromRoute: this.previousRoute,
2174
+ toRoute: this.currentRoute
2175
+ });
2176
+ }
2177
+ else if (wasSearchRoute && !this.isSearchRoute) {
2178
+ // Navigated away from search
2179
+ this.emitShellEvent(ShellSearchEventType.NAVIGATION_AWAY_FROM_SEARCH, {
2180
+ fromRoute: this.previousRoute,
2181
+ toRoute: this.currentRoute
2182
+ });
2183
+ }
2184
+ else if (this.isSearchRoute) {
2185
+ // Navigation within search (filter/sort changes)
2186
+ this.detectSearchNavigationType();
2187
+ }
2188
+ }
2189
+ /**
2190
+ * Detect type of navigation within search page
2191
+ */
2192
+ detectSearchNavigationType() {
2193
+ const currentParams = this.getQueryParams();
2194
+ const urlPath = this.currentRoute.split("?")[0];
2195
+ // Check if it's a direct access or reload
2196
+ if (this.isDirectAccess()) {
2197
+ this.emitShellEvent(ShellSearchEventType.ROUTE_ACCESSED_DIRECTLY, {
2198
+ route: urlPath,
2199
+ queryParams: currentParams
2200
+ });
2201
+ }
2202
+ else if (this.isPageReload()) {
2203
+ this.emitShellEvent(ShellSearchEventType.ROUTE_RELOADED, {
2204
+ route: urlPath,
2205
+ queryParams: currentParams
2206
+ });
2207
+ }
2208
+ }
2209
+ /**
2210
+ * Check if current route is search route
2211
+ */
2212
+ checkIfSearchRoute(route) {
2213
+ return route.includes("/tim-kiem");
2214
+ }
2215
+ /**
2216
+ * Check if this is direct access (no previous route or referrer)
2217
+ */
2218
+ isDirectAccess() {
2219
+ return (!this.previousRoute ||
2220
+ (typeof window !== "undefined" && !document.referrer) ||
2221
+ (typeof window !== "undefined" && !window.history.length));
2222
+ }
2223
+ /**
2224
+ * Check if this is a page reload
2225
+ */
2226
+ isPageReload() {
2227
+ if (typeof window !== "undefined" && typeof performance !== "undefined") {
2228
+ const navigation = performance.navigation;
2229
+ return navigation?.type === "reload";
2230
+ }
2231
+ return false;
2232
+ }
2233
+ /**
2234
+ * Get current query parameters
2235
+ */
2236
+ getQueryParams() {
2237
+ const urlTree = this.router.parseUrl(this.currentRoute);
2238
+ return urlTree.queryParams;
2239
+ }
2240
+ /**
2241
+ * Initialize postMessage listener for MFE communication
2242
+ */
2243
+ initializePostMessageListener() {
2244
+ if (typeof window === "undefined")
2245
+ return;
2246
+ fromEvent(window, "message")
2247
+ .pipe(filter(event => this.isValidMFEEvent(event)), map(event => event.data.payload), takeUntil(this.destroy$))
2248
+ .subscribe(payload => {
2249
+ this.handleMFEEvent(payload);
2250
+ });
2251
+ }
2252
+ /**
2253
+ * Validate incoming postMessage events
2254
+ */
2255
+ isValidMFEEvent(event) {
2256
+ return (event.data &&
2257
+ event.data.type === "SEARCH_EVENT" &&
2258
+ event.data.payload &&
2259
+ event.data.payload.source === "mfe-search");
2260
+ }
2261
+ /**
2262
+ * Send event to MFE via postMessage
2263
+ */
2264
+ postMessageToMFE(event) {
2265
+ if (typeof window === "undefined")
2266
+ return;
2267
+ // Find MFE iframes or windows
2268
+ const mfeFrames = document.querySelectorAll('iframe[src*="5201"], iframe[src*="mfe-search"]');
2269
+ mfeFrames.forEach(frame => {
2270
+ try {
2271
+ frame.contentWindow?.postMessage({
2272
+ type: "SEARCH_EVENT",
2273
+ payload: event
2274
+ }, "*");
2275
+ }
2276
+ catch (error) {
2277
+ console.warn("[SearchEventBridge] Failed to send message to MFE:", error);
2278
+ }
2279
+ });
2280
+ // Also try window.parent in case Shell is embedded
2281
+ try {
2282
+ window.parent?.postMessage({
2283
+ type: "SEARCH_EVENT",
2284
+ payload: event
2285
+ }, "*");
2286
+ }
2287
+ catch (error) {
2288
+ // Ignore cross-origin errors
2289
+ }
2290
+ }
2291
+ /**
2292
+ * Generate correlation ID for event tracking
2293
+ */
2294
+ generateCorrelationId() {
2295
+ return `shell_${Date.now()}_${Math.random().toString(36).substring(2)}`;
2296
+ }
2297
+ /**
2298
+ * Get event history for debugging
2299
+ */
2300
+ getEventHistory() {
2301
+ // Implementation would store recent events for debugging
2302
+ return [];
2303
+ }
2304
+ /**
2305
+ * Public method to handle events from MFE (called by wrapper)
2306
+ */
2307
+ handleMFEEvent(payload) {
2308
+ // Re-emit the event for Shell consumers
2309
+ this.eventSubject.next(payload);
2310
+ // Handle specific events that require Shell action
2311
+ switch (payload.type) {
2312
+ case ShellSearchEventType.NAVIGATION_TO_SEARCH:
2313
+ // Handle navigation request from MFE
2314
+ if (payload.data?.searchQuery) {
2315
+ this.navigateToSearch(payload.data.searchQuery, payload.data?.additionalParams);
2316
+ }
2317
+ else if (payload.data?.queryParams) {
2318
+ // For cases like image search where only queryParams are provided
2319
+ this.router.navigate(["/tim-kiem"], {
2320
+ queryParams: payload.data.queryParams
2321
+ });
2322
+ }
2323
+ break;
2324
+ case ShellSearchEventType.NAVIGATION_AWAY_FROM_SEARCH:
2325
+ // Handle navigation away from search
2326
+ if (payload.data?.targetRoute) {
2327
+ this.router.navigate([payload.data.targetRoute]);
2328
+ }
2329
+ break;
2330
+ case ShellSearchEventType.NAVIGATION_REQUEST:
2331
+ // Handle general navigation request from MFE
2332
+ if (payload.data?.targetRoute) {
2333
+ this.router.navigate([payload.data.targetRoute]);
2334
+ }
2335
+ break;
2336
+ case ShellSearchEventType.SEARCH_INITIATED:
2337
+ // Update URL if needed
2338
+ if (payload.data?.searchQuery) {
2339
+ this.updateSearchQuery(payload.data.searchQuery, true);
2340
+ }
2341
+ break;
2342
+ case ShellSearchEventType.FILTER_CHANGED:
2343
+ case ShellSearchEventType.SORT_CHANGED:
2344
+ // These are handled by the MFE but can trigger Shell-level analytics
2345
+ break;
2346
+ }
2347
+ }
2348
+ ngOnDestroy() {
2349
+ this.destroy$.next();
2350
+ this.destroy$.complete();
2351
+ }
2352
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchEventBridgeService, deps: [{ token: i1$1.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
2353
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchEventBridgeService, providedIn: "root" }); }
2354
+ }
2355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchEventBridgeService, decorators: [{
2356
+ type: Injectable,
2357
+ args: [{
2358
+ providedIn: "root"
2359
+ }]
2360
+ }], ctorParameters: () => [{ type: i1$1.Router }] });
2361
+ /**
2362
+ * Factory function for providing the service
2363
+ */
2364
+ function provideSearchEventBridge() {
2365
+ return {
2366
+ provide: SearchEventBridgeService,
2367
+ useClass: SearchEventBridgeService
2368
+ };
2369
+ }
2370
+
2371
+ var NgxSeoMetaTagAttr;
2372
+ (function (NgxSeoMetaTagAttr) {
2373
+ NgxSeoMetaTagAttr["name"] = "name";
2374
+ NgxSeoMetaTagAttr["property"] = "property";
2375
+ })(NgxSeoMetaTagAttr || (NgxSeoMetaTagAttr = {}));
2376
+ class SeoService {
2377
+ constructor(meta, title,
2378
+ // tslint:disable-next-line: variable-name
2379
+ _document) {
2380
+ this.meta = meta;
2381
+ this.title = title;
2382
+ this._document = _document;
2383
+ }
2384
+ setData(data) {
2385
+ this.setSection(data.section);
2386
+ if (typeof data.keywords !== "undefined") {
2387
+ this.setKeywords(data.keywords);
2388
+ }
2389
+ //this.setKeywords(data.keywords);
2390
+ this.setTitle(data.title);
2391
+ this.setType(data.type);
2392
+ this.setDescription(data.description);
2393
+ this.setImage(data.image);
2394
+ this.setUrl(data.url);
2395
+ this.setPublished(data.published);
2396
+ this.setModified(data.modified);
2397
+ this.setAuthor(data.author);
2398
+ }
2399
+ setKeywords(keywords) {
2400
+ if (Boolean(keywords)) {
2401
+ this.meta.updateTag({ name: "keywords", content: keywords });
2402
+ }
2403
+ else {
2404
+ this.meta.removeTag(`name='keywords'`);
2405
+ }
2406
+ }
2407
+ setSection(section) {
2408
+ if (Boolean(section)) {
2409
+ //this.meta.updateTag({ name: 'article:section', content: section });
2410
+ if (typeof section !== "undefined") {
2411
+ this.meta.updateTag({ name: "article:section", content: section });
2412
+ }
2413
+ }
2414
+ else {
2415
+ this.meta.removeTag(`name='article:section'`);
2416
+ }
2417
+ }
2418
+ setTitle(title = "") {
2419
+ this.title.setTitle(title);
2420
+ if (title && title.length) {
2421
+ this.meta.updateTag({ name: "twitter:title", content: title });
2422
+ this.meta.updateTag({ name: "twitter:image:alt", content: title });
2423
+ this.meta.updateTag({ property: "og:image:alt", content: title });
2424
+ this.meta.updateTag({ property: "og:title", content: title });
2425
+ this.meta.updateTag({ name: "title", content: title });
2426
+ }
2427
+ else {
2428
+ this.meta.removeTag(`name='twitter:title'`);
2429
+ this.meta.removeTag(`name='twitter:image:alt'`);
2430
+ this.meta.removeTag(`property='og:image:alt'`);
2431
+ this.meta.removeTag(`property='og:title'`);
2432
+ this.meta.removeTag(`name='title'`);
2433
+ }
2434
+ }
2435
+ setType(type) {
2436
+ if (type && type.length) {
2437
+ this.meta.updateTag({ property: "og:type", content: type });
2438
+ }
2439
+ else {
2440
+ this.meta.removeTag(`property='og:type'`);
2441
+ }
2442
+ }
2443
+ setDescription(description) {
2444
+ if (description && description.length) {
2445
+ this.meta.updateTag({
2446
+ name: "twitter:description",
2447
+ content: description
2448
+ });
2449
+ this.meta.updateTag({ property: "og:description", content: description });
2450
+ this.meta.updateTag({ name: "description", content: description });
2451
+ }
2452
+ else {
2453
+ this.meta.removeTag(`name='twitter:description'`);
2454
+ this.meta.removeTag(`property='og:description'`);
2455
+ this.meta.removeTag(`name='description'`);
2456
+ }
2457
+ }
2458
+ setImage(image) {
2459
+ if (image && image.length) {
2460
+ this.meta.updateTag({ name: "twitter:image", content: image });
2461
+ this.meta.updateTag({ property: "og:image", content: image });
2462
+ this.meta.updateTag({ property: "og:image:height", content: "630" });
2463
+ }
2464
+ else {
2465
+ this.meta.removeTag(`name='twitter:image'`);
2466
+ this.meta.removeTag(`property='og:image'`);
2467
+ this.meta.removeTag(`property='og:image:height'`);
2468
+ }
2469
+ }
2470
+ setUrl(url) {
2471
+ if (url && url.length) {
2472
+ this.meta.updateTag({ property: "og:url", content: url });
2473
+ }
2474
+ else {
2475
+ this.meta.removeTag(`property='og:url'`);
2476
+ }
2477
+ this.setCanonicalUrl(url);
2478
+ }
2479
+ setPublished(publishedDateString) {
2480
+ if (publishedDateString) {
2481
+ const publishedDate = new Date(publishedDateString);
2482
+ this.meta.updateTag({
2483
+ name: "article:published_time",
2484
+ content: publishedDate.toISOString()
2485
+ });
2486
+ this.meta.updateTag({
2487
+ name: "published_date",
2488
+ content: publishedDate.toISOString()
2489
+ });
2490
+ }
2491
+ else {
2492
+ this.meta.removeTag(`name='article:published_time'`);
2493
+ this.meta.removeTag(`name='publication_date'`);
2494
+ }
2495
+ }
2496
+ setModified(modifiedDateString) {
2497
+ if (modifiedDateString) {
2498
+ const modifiedDate = new Date(modifiedDateString);
2499
+ this.meta.updateTag({
2500
+ name: "article:modified_time",
2501
+ content: modifiedDate.toISOString()
2502
+ });
2503
+ this.meta.updateTag({
2504
+ name: "og:updated_time",
2505
+ content: modifiedDate.toISOString()
2506
+ });
2507
+ }
2508
+ else {
2509
+ this.meta.removeTag(`name='article:modified_time'`);
2510
+ this.meta.removeTag(`name='og:updated_time'`);
2511
+ }
2512
+ }
2513
+ setAuthor(author) {
2514
+ if (author && author.length) {
2515
+ this.meta.updateTag({ name: "article:author", content: author });
2516
+ this.meta.updateTag({ name: "author", content: author });
2517
+ }
2518
+ else {
2519
+ this.meta.removeTag(`name='article:author'`);
2520
+ this.meta.removeTag(`name='author'`);
2521
+ }
2522
+ }
2523
+ setTwitterSiteCreator(site) {
2524
+ if (Boolean(site)) {
2525
+ //this.meta.updateTag({ name: 'twitter:site', content: site });
2526
+ if (typeof site !== "undefined") {
2527
+ this.meta.updateTag({ name: "twitter:site", content: site });
2528
+ this.meta.updateTag({ name: "twitter:creator", content: site });
2529
+ }
2530
+ }
2531
+ else {
2532
+ this.meta.removeTag(`name='twitter:site'`);
2533
+ this.meta.removeTag(`name='twitter:creator'`);
2534
+ }
2535
+ }
2536
+ setTwitterCard(card) {
2537
+ if (Boolean(card)) {
2538
+ if (typeof card !== "undefined") {
2539
+ this.meta.updateTag({ name: "twitter:card", content: card });
2540
+ }
2541
+ }
2542
+ else {
2543
+ this.meta.removeTag(`name='twitter:card'`);
2544
+ }
2545
+ }
2546
+ setFbAppId(appId) {
2547
+ if (Boolean(appId)) {
2548
+ if (typeof appId !== "undefined") {
2549
+ this.meta.updateTag({ property: "fb:app_id", content: appId });
2550
+ }
2551
+ }
2552
+ else {
2553
+ this.meta.removeTag(`property='fb:app_id'`);
2554
+ }
2555
+ }
2556
+ setMetaTag(metaTag) {
2557
+ if (Boolean(metaTag.value)) {
2558
+ if (typeof metaTag !== "undefined" &&
2559
+ typeof metaTag.attr !== "undefined" &&
2560
+ typeof metaTag.attrValue !== "undefined" &&
2561
+ typeof metaTag.value !== "undefined") {
2562
+ const metaTagObject = {
2563
+ [metaTag.attr]: metaTag.attrValue,
2564
+ content: metaTag.value
2565
+ };
2566
+ this.meta.updateTag(metaTagObject);
2567
+ }
2568
+ }
2569
+ else {
2570
+ const selector = `${metaTag.attr}='${metaTag.attrValue}'`;
2571
+ this.meta.removeTag(selector);
2572
+ }
2573
+ }
2574
+ setMetaTags(metaTags) {
2575
+ for (const metaTag of metaTags) {
2576
+ this.setMetaTag(metaTag);
2577
+ }
2578
+ }
2579
+ setCanonicalUrl(url) {
2580
+ // first remove potential previous url
2581
+ const selector = `link[rel='canonical']`;
2582
+ const canonicalElement = this._document.head.querySelector(selector);
2583
+ if (canonicalElement) {
2584
+ this._document.head.removeChild(canonicalElement);
2585
+ }
2586
+ if (url && url.length) {
2587
+ const link = this._document.createElement("link");
2588
+ link.setAttribute("rel", "canonical");
2589
+ this._document.head.appendChild(link);
2590
+ link.setAttribute("href", url);
2591
+ }
2592
+ }
2593
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SeoService, deps: [{ token: i1$4.Meta }, { token: i1$4.Title }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
2594
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SeoService, providedIn: "root" }); }
2595
+ }
2596
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SeoService, decorators: [{
2597
+ type: Injectable,
2598
+ args: [{
2599
+ providedIn: "root"
2600
+ }]
2601
+ }], ctorParameters: () => [{ type: i1$4.Meta }, { type: i1$4.Title }, { type: Document, decorators: [{
2602
+ type: Inject,
2603
+ args: [DOCUMENT]
2604
+ }] }] });
2605
+
2606
+ class SessionStorageService {
2607
+ constructor(platformService) {
2608
+ this.platformService = platformService;
2609
+ }
2610
+ setItem(key, value) {
2611
+ this.platformService.runOnBrowser(() => {
2612
+ sessionStorage.setItem(key, value);
2613
+ });
2614
+ }
2615
+ getItem(key) {
2616
+ return this.platformService.runOnBrowser(() => {
2617
+ return sessionStorage.getItem(key);
2618
+ });
2619
+ }
2620
+ removeItem(key) {
2621
+ this.platformService.runOnBrowser(() => {
2622
+ sessionStorage.removeItem(key);
2623
+ });
2624
+ }
2625
+ clear() {
2626
+ this.platformService.runOnBrowser(() => {
2627
+ sessionStorage.clear();
2628
+ });
2629
+ }
2630
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SessionStorageService, deps: [{ token: PlatformService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2631
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SessionStorageService, providedIn: "root" }); }
2632
+ }
2633
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SessionStorageService, decorators: [{
2634
+ type: Injectable,
2635
+ args: [{ providedIn: "root" }]
2636
+ }], ctorParameters: () => [{ type: PlatformService }] });
2637
+
2638
+ class WindowService {
2639
+ constructor(platformService) {
2640
+ this.platformService = platformService;
2641
+ }
2642
+ get nativeWindow() {
2643
+ return this.platformService.getWindow();
2644
+ }
2645
+ get configApp() {
2646
+ return this.nativeWindow?.__CONFIG_APP__ ?? null;
2647
+ }
2648
+ getConfigValue(key) {
2649
+ return this.configApp?.[key] ?? null;
2650
+ }
2651
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WindowService, deps: [{ token: PlatformService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2652
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WindowService, providedIn: "root" }); }
2653
+ }
2654
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WindowService, decorators: [{
2655
+ type: Injectable,
2656
+ args: [{ providedIn: "root" }]
2657
+ }], ctorParameters: () => [{ type: PlatformService }] });
2658
+
2659
+ /*
2660
+ * Public API Surface of core/infrastructure
2661
+ */
2662
+ // Guards
2663
+
2664
+ /**
2665
+ * Generated bundle index. Do not edit.
2666
+ */
2667
+
2668
+ export { API_SERVICE_CONFIG, APP_ENVIRONMENT, AUTH_DIALOG_SERVICE, ApiService, AppConfigService, AppInitializeService, AuthGuard, AuthService, BreakpointService, CacheService, CookieService, DocumentService, FileAIA_GCSService, FileGCSService, FileService, FileWAPGCSService, LoaderService, LoadingService, LoadingSpinnerService, LocalStorageService, NgxSeoMetaTagAttr, OverlayService, PlatformService, RequestCacheService, ResponsiveService, SearchEventBridgeService, SeoService, SessionStorageService, SessionValidationError, ShellSearchEventType, StorageService, WindowService, authInterceptor, duplicateRequestInterceptor, errorInterceptor, loadingInterceptor, provideSearchEventBridge };
2669
+ //# sourceMappingURL=web-portal-core-infrastructure.mjs.map