@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.
- package/fesm2022/web-portal-core-infrastructure.mjs +2669 -0
- package/fesm2022/web-portal-core-infrastructure.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/guards/auth.guard.d.ts +17 -0
- package/lib/interceptors/auth.interceptor.d.ts +2 -0
- package/lib/interceptors/duplicate-request.interceptor.d.ts +2 -0
- package/lib/interceptors/error.interceptor.d.ts +6 -0
- package/lib/interceptors/loading-bar.interceptor.d.ts +2 -0
- package/lib/services/api.service.d.ts +193 -0
- package/lib/services/app-config.service.d.ts +65 -0
- package/lib/services/app-initialize.service.d.ts +14 -0
- package/lib/services/auth/auth-dialog.interface.d.ts +13 -0
- package/lib/services/auth/auth.service.d.ts +22 -0
- package/lib/services/auth/storage.service.d.ts +12 -0
- package/lib/services/breakpoint.service.d.ts +20 -0
- package/lib/services/cache.service.d.ts +49 -0
- package/lib/services/cookie.service.d.ts +12 -0
- package/lib/services/document.service.d.ts +9 -0
- package/lib/services/file-aia-gcs.service.d.ts +15 -0
- package/lib/services/file-gcs.service.d.ts +15 -0
- package/lib/services/file-wap-gcs.service.d.ts +31 -0
- package/lib/services/file.service.d.ts +17 -0
- package/lib/services/loader.service.d.ts +9 -0
- package/lib/services/loading-spinner.service.d.ts +9 -0
- package/lib/services/loading.service.d.ts +10 -0
- package/lib/services/local-storage.service.d.ts +13 -0
- package/lib/services/notification.service.d.ts +1 -0
- package/lib/services/overlay.service.d.ts +13 -0
- package/lib/services/platform.service.d.ts +16 -0
- package/lib/services/request-cache.service.d.ts +56 -0
- package/lib/services/responsive.service.d.ts +11 -0
- package/lib/services/search-event-bridge.service.d.ts +146 -0
- package/lib/services/seo.service.d.ts +37 -0
- package/lib/services/session-storage.service.d.ts +12 -0
- package/lib/services/window.service.d.ts +22 -0
- package/package.json +32 -0
- 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
|