@factor_ec/utils 5.0.9 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/fesm2022/factor_ec-utils.mjs +1051 -306
- package/fesm2022/factor_ec-utils.mjs.map +1 -1
- package/package.json +5 -5
- package/types/factor_ec-utils.d.ts +894 -0
- package/index.d.ts +0 -5
- package/lib/array.service.d.ts +0 -6
- package/lib/color.service.d.ts +0 -58
- package/lib/csv.service.d.ts +0 -88
- package/lib/date.service.d.ts +0 -6
- package/lib/file-picker.service.d.ts +0 -10
- package/lib/files.service.d.ts +0 -14
- package/lib/google-tag-manager.service.d.ts +0 -18
- package/lib/models/auth-token-payload.d.ts +0 -6
- package/lib/models/auth-token.d.ts +0 -4
- package/lib/models/currency.d.ts +0 -6
- package/lib/models/error.d.ts +0 -6
- package/lib/models/language.d.ts +0 -4
- package/lib/models/login.d.ts +0 -4
- package/lib/models/operation.d.ts +0 -4
- package/lib/models/option.d.ts +0 -5
- package/lib/models/user.d.ts +0 -5
- package/lib/object.service.d.ts +0 -7
- package/lib/storage.service.d.ts +0 -12
- package/lib/string.service.d.ts +0 -19
- package/lib/validators/ec/identification-type.d.ts +0 -1
- package/lib/validators/ec/identification-validator.d.ts +0 -2
- package/public-api.d.ts +0 -21
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, PLATFORM_ID,
|
|
2
|
+
import { Injectable, inject, PLATFORM_ID, InjectionToken, signal, computed } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
5
|
+
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
|
6
|
+
import { MatDialog } from '@angular/material/dialog';
|
|
7
|
+
import { BehaviorSubject, switchMap, throwError, catchError, share, finalize, filter, take, lastValueFrom, tap } from 'rxjs';
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Utility service for array operations.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Provides helper methods for common array manipulation tasks.
|
|
14
|
+
*/
|
|
15
|
+
class ArrayUtil {
|
|
16
|
+
/**
|
|
17
|
+
* Merges multiple arrays into a single array, combining objects with the same property value.
|
|
18
|
+
*
|
|
19
|
+
* @param arrays - Array of arrays to merge
|
|
20
|
+
* @param prop - Property name to use as the key for merging objects
|
|
21
|
+
* @returns Merged array with combined objects
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const arr1 = [{ id: 1, name: 'John' }];
|
|
26
|
+
* const arr2 = [{ id: 1, age: 30 }];
|
|
27
|
+
* const result = arrayUtil.merge([arr1, arr2], 'id');
|
|
28
|
+
* // Returns: [{ id: 1, name: 'John', age: 30 }]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
8
31
|
merge(arrays, prop) {
|
|
9
32
|
const merged = {};
|
|
10
33
|
arrays.forEach(arr => {
|
|
@@ -14,17 +37,31 @@ class ArrayService {
|
|
|
14
37
|
});
|
|
15
38
|
return Object.values(merged);
|
|
16
39
|
}
|
|
17
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
18
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
40
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ArrayUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
41
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ArrayUtil, providedIn: 'root' });
|
|
19
42
|
}
|
|
20
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
43
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ArrayUtil, decorators: [{
|
|
21
44
|
type: Injectable,
|
|
22
45
|
args: [{
|
|
23
|
-
providedIn: 'root'
|
|
46
|
+
providedIn: 'root',
|
|
24
47
|
}]
|
|
25
48
|
}] });
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Utility service for color operations and hash-based color generation.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Provides methods to generate consistent colors from strings using hash algorithms,
|
|
55
|
+
* and convert between different color formats (HSL, RGB, HEX).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const colorUtil = inject(ColorUtil);
|
|
60
|
+
* const hexColor = colorUtil.hex('username');
|
|
61
|
+
* // Returns: '#a1b2c3' (consistent color for the same string)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
class ColorUtil {
|
|
28
65
|
L;
|
|
29
66
|
S;
|
|
30
67
|
hueRanges;
|
|
@@ -53,9 +90,13 @@ class ColorService {
|
|
|
53
90
|
});
|
|
54
91
|
}
|
|
55
92
|
/**
|
|
56
|
-
* BKDR
|
|
93
|
+
* Generates a hash value from a string using BKDR hash algorithm (modified version).
|
|
94
|
+
*
|
|
95
|
+
* @param str - The string to hash
|
|
96
|
+
* @returns A numeric hash value
|
|
57
97
|
*
|
|
58
|
-
* @
|
|
98
|
+
* @remarks
|
|
99
|
+
* This is a modified BKDR hash algorithm optimized for short strings.
|
|
59
100
|
*/
|
|
60
101
|
hash(str) {
|
|
61
102
|
let seed = 131;
|
|
@@ -74,10 +115,10 @@ class ColorService {
|
|
|
74
115
|
return hash;
|
|
75
116
|
}
|
|
76
117
|
/**
|
|
77
|
-
*
|
|
118
|
+
* Converts an RGB array to a hexadecimal color string.
|
|
78
119
|
*
|
|
79
|
-
* @param RGBArray - [R, G, B]
|
|
80
|
-
* @returns
|
|
120
|
+
* @param RGBArray - Array with [R, G, B] values (0-255)
|
|
121
|
+
* @returns Hexadecimal color string starting with # (e.g., '#a1b2c3')
|
|
81
122
|
*/
|
|
82
123
|
rgb2hex(RGBArray) {
|
|
83
124
|
let hex = '#';
|
|
@@ -90,13 +131,14 @@ class ColorService {
|
|
|
90
131
|
return hex;
|
|
91
132
|
}
|
|
92
133
|
/**
|
|
93
|
-
*
|
|
134
|
+
* Converts HSL color values to RGB array.
|
|
135
|
+
*
|
|
136
|
+
* @param H - Hue value in range [0, 360)
|
|
137
|
+
* @param S - Saturation value in range [0, 1]
|
|
138
|
+
* @param L - Lightness value in range [0, 1]
|
|
139
|
+
* @returns Array with [R, G, B] values in range [0, 255]
|
|
94
140
|
*
|
|
95
141
|
* @see {@link http://zh.wikipedia.org/wiki/HSL和HSV色彩空间} for further information.
|
|
96
|
-
* @param H Hue ∈ [0, 360)
|
|
97
|
-
* @param S Saturation ∈ [0, 1]
|
|
98
|
-
* @param L Lightness ∈ [0, 1]
|
|
99
|
-
* @returns R, G, B ∈ [0, 255]
|
|
100
142
|
*/
|
|
101
143
|
hsl2rgb(H, S, L) {
|
|
102
144
|
H /= 360;
|
|
@@ -125,11 +167,10 @@ class ColorService {
|
|
|
125
167
|
});
|
|
126
168
|
}
|
|
127
169
|
/**
|
|
128
|
-
*
|
|
129
|
-
* Note that H ∈ [0, 360); S ∈ [0, 1]; L ∈ [0, 1];
|
|
170
|
+
* Generates HSL color values from a string hash.
|
|
130
171
|
*
|
|
131
|
-
* @param str string to
|
|
132
|
-
* @returns [
|
|
172
|
+
* @param str - The string to generate color from
|
|
173
|
+
* @returns Array with [H, S, L] values where H ∈ [0, 360), S ∈ [0, 1], L ∈ [0, 1]
|
|
133
174
|
*/
|
|
134
175
|
hsl(str) {
|
|
135
176
|
let H;
|
|
@@ -155,86 +196,135 @@ class ColorService {
|
|
|
155
196
|
return [H, S, L];
|
|
156
197
|
}
|
|
157
198
|
/**
|
|
158
|
-
*
|
|
159
|
-
* Note that R, G, B ∈ [0, 255]
|
|
199
|
+
* Generates RGB color values from a string hash.
|
|
160
200
|
*
|
|
161
|
-
* @param str string to
|
|
162
|
-
* @returns [
|
|
201
|
+
* @param str - The string to generate color from
|
|
202
|
+
* @returns Array with [R, G, B] values in range [0, 255]
|
|
163
203
|
*/
|
|
164
204
|
rgb(str) {
|
|
165
205
|
let hsl = this.hsl(str);
|
|
166
206
|
return this.hsl2rgb(hsl[0], hsl[1], hsl[2]);
|
|
167
207
|
}
|
|
168
208
|
/**
|
|
169
|
-
*
|
|
209
|
+
* Generates a hexadecimal color string from a string hash.
|
|
210
|
+
*
|
|
211
|
+
* @param str - The string to generate color from
|
|
212
|
+
* @returns Hexadecimal color string starting with # (e.g., '#a1b2c3')
|
|
170
213
|
*
|
|
171
|
-
* @
|
|
172
|
-
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* const color = colorUtil.hex('username');
|
|
217
|
+
* // Returns: '#a1b2c3' (same string always returns same color)
|
|
218
|
+
* ```
|
|
173
219
|
*/
|
|
174
220
|
hex(str) {
|
|
175
221
|
let rgb = this.rgb(str);
|
|
176
222
|
return this.rgb2hex(rgb);
|
|
177
223
|
}
|
|
178
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
179
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
224
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ColorUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
225
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ColorUtil, providedIn: 'root' });
|
|
180
226
|
}
|
|
181
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
227
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ColorUtil, decorators: [{
|
|
182
228
|
type: Injectable,
|
|
183
229
|
args: [{
|
|
184
230
|
providedIn: 'root',
|
|
185
231
|
}]
|
|
186
232
|
}], ctorParameters: () => [] });
|
|
187
233
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Default configuration constants for CSV export operations.
|
|
236
|
+
*/
|
|
237
|
+
const CSV_CONFIG = {
|
|
238
|
+
/** End of line character for CSV */
|
|
239
|
+
EOL: '\r\n',
|
|
240
|
+
/** Byte Order Mark for UTF-8 encoding */
|
|
241
|
+
BOM: '\ufeff',
|
|
242
|
+
/** Default field separator character */
|
|
243
|
+
DEFAULT_FIELD_SEPARATOR: ',',
|
|
244
|
+
/** Default decimal separator */
|
|
245
|
+
DEFAULT_DECIMAL_SEPARATOR: '.',
|
|
246
|
+
/** Default quote character for strings */
|
|
247
|
+
DEFAULT_QUOTE: '"',
|
|
248
|
+
/** Default value for showing title */
|
|
249
|
+
DEFAULT_SHOW_TITLE: false,
|
|
250
|
+
/** Default title text */
|
|
251
|
+
DEFAULT_TITLE: 'My Generated Report',
|
|
252
|
+
/** Default filename (without extension) */
|
|
253
|
+
DEFAULT_FILENAME: 'generated',
|
|
254
|
+
/** Default value for showing column labels */
|
|
255
|
+
DEFAULT_SHOW_LABELS: false,
|
|
256
|
+
/** Default value for using text file format */
|
|
257
|
+
DEFAULT_USE_TEXT_FILE: false,
|
|
258
|
+
/** Default value for including BOM */
|
|
259
|
+
DEFAULT_USE_BOM: true,
|
|
260
|
+
/** Default header array (empty) */
|
|
261
|
+
DEFAULT_HEADER: [],
|
|
262
|
+
/** Default value for using object keys as headers */
|
|
263
|
+
DEFAULT_KEYS_AS_HEADERS: false
|
|
215
264
|
};
|
|
216
|
-
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Service for exporting data to CSV format and parsing CSV content.
|
|
268
|
+
*
|
|
269
|
+
* @remarks
|
|
270
|
+
* Provides functionality to generate CSV files from JSON data with customizable options,
|
|
271
|
+
* and to parse CSV content back into structured data.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const csvExporter = inject(CsvExporter);
|
|
276
|
+
* csvExporter.options = { filename: 'export', showLabels: true };
|
|
277
|
+
* csvExporter.generate([{ name: 'John', age: 30 }]);
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
class CsvExporter {
|
|
217
281
|
_data = [];
|
|
218
282
|
_options;
|
|
219
283
|
_csv = '';
|
|
284
|
+
configDefaults = {
|
|
285
|
+
filename: CSV_CONFIG.DEFAULT_FILENAME,
|
|
286
|
+
fieldSeparator: CSV_CONFIG.DEFAULT_FIELD_SEPARATOR,
|
|
287
|
+
quoteStrings: CSV_CONFIG.DEFAULT_QUOTE,
|
|
288
|
+
decimalSeparator: CSV_CONFIG.DEFAULT_DECIMAL_SEPARATOR,
|
|
289
|
+
showLabels: CSV_CONFIG.DEFAULT_SHOW_LABELS,
|
|
290
|
+
showTitle: CSV_CONFIG.DEFAULT_SHOW_TITLE,
|
|
291
|
+
title: CSV_CONFIG.DEFAULT_TITLE,
|
|
292
|
+
useTextFile: CSV_CONFIG.DEFAULT_USE_TEXT_FILE,
|
|
293
|
+
useBom: CSV_CONFIG.DEFAULT_USE_BOM,
|
|
294
|
+
headers: [...CSV_CONFIG.DEFAULT_HEADER],
|
|
295
|
+
useKeysAsHeaders: CSV_CONFIG.DEFAULT_KEYS_AS_HEADERS
|
|
296
|
+
};
|
|
220
297
|
get options() {
|
|
221
298
|
return this._options;
|
|
222
299
|
}
|
|
223
300
|
set options(options) {
|
|
224
|
-
this._options = this.objectAssign({},
|
|
301
|
+
this._options = this.objectAssign({}, this.configDefaults, options);
|
|
225
302
|
}
|
|
226
303
|
constructor() {
|
|
227
|
-
this._options = this.objectAssign({},
|
|
304
|
+
this._options = this.objectAssign({}, this.configDefaults);
|
|
228
305
|
}
|
|
229
306
|
/**
|
|
230
|
-
*
|
|
307
|
+
* Generates a CSV file from JSON data and optionally downloads it.
|
|
308
|
+
*
|
|
309
|
+
* @param jsonData - The data to export (array of objects or JSON string)
|
|
310
|
+
* @param shouldReturnCsv - If true, returns the CSV string instead of downloading
|
|
311
|
+
* @returns The CSV string if shouldReturnCsv is true, otherwise void
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* // Download CSV
|
|
316
|
+
* csvExporter.generate([{ name: 'John', age: 30 }]);
|
|
317
|
+
*
|
|
318
|
+
* // Get CSV string
|
|
319
|
+
* const csvString = csvExporter.generate([{ name: 'John' }], true);
|
|
320
|
+
* ```
|
|
231
321
|
*/
|
|
232
322
|
generate(jsonData, shouldReturnCsv = false) {
|
|
233
323
|
// Make sure to reset csv data on each run
|
|
234
324
|
this._csv = '';
|
|
235
325
|
this._parseData(jsonData);
|
|
236
326
|
if (this._options.useBom) {
|
|
237
|
-
this._csv +=
|
|
327
|
+
this._csv += CSV_CONFIG.BOM;
|
|
238
328
|
}
|
|
239
329
|
if (this._options.showTitle) {
|
|
240
330
|
this._csv += this._options.title + '\r\n\n';
|
|
@@ -257,8 +347,6 @@ class CsvService {
|
|
|
257
347
|
const blob = new Blob([this._csv], {
|
|
258
348
|
type: 'text/' + FileType + ';charset=utf8;'
|
|
259
349
|
});
|
|
260
|
-
//const attachmentType = this._options.useTextFile ? 'text' : 'csv';
|
|
261
|
-
// const uri = 'data:attachment/' + attachmentType + ';charset=utf-8,' + encodeURI(this._csv);
|
|
262
350
|
const link = document.createElement('a');
|
|
263
351
|
link.href = URL.createObjectURL(blob);
|
|
264
352
|
link.setAttribute('visibility', 'hidden');
|
|
@@ -268,7 +356,9 @@ class CsvService {
|
|
|
268
356
|
document.body.removeChild(link);
|
|
269
357
|
}
|
|
270
358
|
/**
|
|
271
|
-
*
|
|
359
|
+
* Creates CSV headers row based on configuration.
|
|
360
|
+
*
|
|
361
|
+
* @private
|
|
272
362
|
*/
|
|
273
363
|
_getHeaders() {
|
|
274
364
|
if (!this._options.showLabels && !this._options.useKeysAsHeaders) {
|
|
@@ -284,11 +374,13 @@ class CsvService {
|
|
|
284
374
|
row += headers[keyPos] + this._options.fieldSeparator;
|
|
285
375
|
}
|
|
286
376
|
row = row.slice(0, -1);
|
|
287
|
-
this._csv += row +
|
|
377
|
+
this._csv += row + CSV_CONFIG.EOL;
|
|
288
378
|
}
|
|
289
379
|
}
|
|
290
380
|
/**
|
|
291
|
-
*
|
|
381
|
+
* Creates CSV body rows from the data.
|
|
382
|
+
*
|
|
383
|
+
* @private
|
|
292
384
|
*/
|
|
293
385
|
_getBody() {
|
|
294
386
|
const keys = Object.keys(this._data[0]);
|
|
@@ -300,12 +392,15 @@ class CsvService {
|
|
|
300
392
|
this._formatData(this._data[i][key]) + this._options.fieldSeparator;
|
|
301
393
|
}
|
|
302
394
|
row = row.slice(0, -1);
|
|
303
|
-
this._csv += row +
|
|
395
|
+
this._csv += row + CSV_CONFIG.EOL;
|
|
304
396
|
}
|
|
305
397
|
}
|
|
306
398
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
399
|
+
* Formats data for CSV output according to configuration.
|
|
400
|
+
*
|
|
401
|
+
* @param data - The data value to format
|
|
402
|
+
* @returns The formatted data as a string
|
|
403
|
+
* @private
|
|
309
404
|
*/
|
|
310
405
|
_formatData(data) {
|
|
311
406
|
if (this._options.decimalSeparator === 'locale' && this._isFloat(data)) {
|
|
@@ -330,27 +425,32 @@ class CsvService {
|
|
|
330
425
|
return data;
|
|
331
426
|
}
|
|
332
427
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
428
|
+
* Checks if a value is a floating point number.
|
|
429
|
+
*
|
|
430
|
+
* @param input - The value to check
|
|
431
|
+
* @returns True if the value is a float, false otherwise
|
|
432
|
+
* @private
|
|
335
433
|
*/
|
|
336
434
|
_isFloat(input) {
|
|
337
435
|
return +input === input && (!isFinite(input) || Boolean(input % 1));
|
|
338
436
|
}
|
|
339
437
|
/**
|
|
340
|
-
*
|
|
438
|
+
* Parses JSON data into an array format.
|
|
341
439
|
*
|
|
440
|
+
* @param jsonData - The JSON data to parse (object, array, or JSON string)
|
|
441
|
+
* @returns Parsed array of data
|
|
342
442
|
* @private
|
|
343
|
-
* @param {*} jsonData
|
|
344
|
-
* @returns {any[]}
|
|
345
|
-
* @memberof ExportToCsv
|
|
346
443
|
*/
|
|
347
444
|
_parseData(jsonData) {
|
|
348
445
|
this._data = typeof jsonData != 'object' ? JSON.parse(jsonData) : jsonData;
|
|
349
446
|
return this._data;
|
|
350
447
|
}
|
|
351
448
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
449
|
+
* Converts a value to an object, throwing an error for null or undefined.
|
|
450
|
+
*
|
|
451
|
+
* @param val - The value to convert
|
|
452
|
+
* @returns The value as an object
|
|
453
|
+
* @throws {TypeError} If val is null or undefined
|
|
354
454
|
*/
|
|
355
455
|
toObject(val) {
|
|
356
456
|
if (val === null || val === undefined) {
|
|
@@ -359,9 +459,11 @@ class CsvService {
|
|
|
359
459
|
return Object(val);
|
|
360
460
|
}
|
|
361
461
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* @param
|
|
462
|
+
* Assigns properties from source objects to a target object (similar to Object.assign).
|
|
463
|
+
*
|
|
464
|
+
* @param target - The target object to assign properties to
|
|
465
|
+
* @param source - One or more source objects to copy properties from
|
|
466
|
+
* @returns The target object with assigned properties
|
|
365
467
|
*/
|
|
366
468
|
objectAssign(target, ...source) {
|
|
367
469
|
let from;
|
|
@@ -387,6 +489,18 @@ class CsvService {
|
|
|
387
489
|
}
|
|
388
490
|
return to;
|
|
389
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Parses CSV content into a structured format with headers and content rows.
|
|
494
|
+
*
|
|
495
|
+
* @param csvContent - The CSV string to parse
|
|
496
|
+
* @returns An object containing the header array and content rows array
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```typescript
|
|
500
|
+
* const parsed = csvExporter.read('name,age\nJohn,30\nJane,25');
|
|
501
|
+
* // Returns: { header: ['name', 'age'], content: [['John', '30'], ['Jane', '25']] }
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
390
504
|
read(csvContent) {
|
|
391
505
|
const lines = csvContent.split('\n');
|
|
392
506
|
let header;
|
|
@@ -396,7 +510,7 @@ class CsvService {
|
|
|
396
510
|
return this.parseLine(line.trim());
|
|
397
511
|
});
|
|
398
512
|
header = csv[0];
|
|
399
|
-
//
|
|
513
|
+
// If a blank line is found, stop reading subsequent lines
|
|
400
514
|
let breakIndex;
|
|
401
515
|
csv.some((row, index) => {
|
|
402
516
|
const isBlankLine = row.every((column) => column.trim() === '');
|
|
@@ -417,236 +531,189 @@ class CsvService {
|
|
|
417
531
|
content
|
|
418
532
|
};
|
|
419
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Parses a single CSV line into an array of values.
|
|
536
|
+
*
|
|
537
|
+
* @param csvLine - A single line of CSV content
|
|
538
|
+
* @returns Array of parsed values from the line
|
|
539
|
+
*
|
|
540
|
+
* @remarks
|
|
541
|
+
* Handles quoted values and different quote styles (single and double quotes).
|
|
542
|
+
*/
|
|
420
543
|
parseLine(csvLine) {
|
|
421
544
|
const values = [];
|
|
422
545
|
const regex = /(?:"([^"]*)"|'([^']*)'|([^,]+))(?:,|\r?$)/g;
|
|
423
546
|
let match;
|
|
424
547
|
while ((match = regex.exec(csvLine)) !== null) {
|
|
425
|
-
values.push(match[1] || match[2] || match[3]); //
|
|
548
|
+
values.push(match[1] || match[2] || match[3]); // Extract the correct value
|
|
426
549
|
}
|
|
427
550
|
return values;
|
|
428
551
|
}
|
|
429
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
430
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
552
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsvExporter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
553
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsvExporter, providedIn: 'root' });
|
|
431
554
|
}
|
|
432
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
555
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsvExporter, decorators: [{
|
|
433
556
|
type: Injectable,
|
|
434
557
|
args: [{
|
|
435
|
-
providedIn: 'root'
|
|
558
|
+
providedIn: 'root',
|
|
436
559
|
}]
|
|
437
560
|
}], ctorParameters: () => [] });
|
|
438
561
|
|
|
439
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Utility service for date operations.
|
|
564
|
+
*
|
|
565
|
+
* @remarks
|
|
566
|
+
* Provides helper methods for date parsing and manipulation.
|
|
567
|
+
*/
|
|
568
|
+
class DateUtil {
|
|
569
|
+
/**
|
|
570
|
+
* Parses a date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss) and returns a Date object.
|
|
571
|
+
*
|
|
572
|
+
* @param date - The date string to parse (ISO format)
|
|
573
|
+
* @returns A Date object representing the parsed date
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```typescript
|
|
577
|
+
* const date = dateUtil.getDate('2025-01-15');
|
|
578
|
+
* // Returns: Date object for January 15, 2025
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
440
581
|
getDate(date) {
|
|
441
582
|
const dateParts = date.split('T')[0].split('-');
|
|
442
583
|
return new Date(Number(dateParts[0]), Number(dateParts[1]) - 1, Number(dateParts[2]));
|
|
443
584
|
}
|
|
444
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
445
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
585
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DateUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
586
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DateUtil, providedIn: 'root' });
|
|
446
587
|
}
|
|
447
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
448
|
-
type: Injectable,
|
|
449
|
-
args: [{
|
|
450
|
-
providedIn: 'root'
|
|
451
|
-
}]
|
|
452
|
-
}] });
|
|
453
|
-
|
|
454
|
-
class FilesService {
|
|
455
|
-
callback;
|
|
456
|
-
fileInput;
|
|
457
|
-
pickerClosed = false;
|
|
458
|
-
constructor() {
|
|
459
|
-
this.fileInput = document.createElement('input');
|
|
460
|
-
this.fileInput.type = 'file';
|
|
461
|
-
this.fileInput.addEventListener('change', (event) => {
|
|
462
|
-
this.pickerClosed = true;
|
|
463
|
-
this.loadValue(event.currentTarget.files);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
loadValue(files) {
|
|
467
|
-
if (files && files.length > 0) {
|
|
468
|
-
let data = [];
|
|
469
|
-
for (let i = 0; i < files.length; i++) {
|
|
470
|
-
const file = files.item(i);
|
|
471
|
-
const reader = new FileReader();
|
|
472
|
-
reader.readAsDataURL(file);
|
|
473
|
-
reader.onload = () => {
|
|
474
|
-
data.push(Object.assign(file, {
|
|
475
|
-
data: reader.result,
|
|
476
|
-
}));
|
|
477
|
-
if (data.length == files.length) {
|
|
478
|
-
this.callback(data.length > 0 ? data : null);
|
|
479
|
-
this.fileInput.value = '';
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
this.callback(null);
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
open(callback, options) {
|
|
489
|
-
this.pickerClosed = false;
|
|
490
|
-
// Detectar cuando el usuario vuelve a la ventana después de abrir el picker
|
|
491
|
-
const onFocus = () => {
|
|
492
|
-
setTimeout(() => {
|
|
493
|
-
if (!this.pickerClosed) {
|
|
494
|
-
console.log('El usuario cerró el file picker sin seleccionar archivos.');
|
|
495
|
-
this.loadValue(null);
|
|
496
|
-
}
|
|
497
|
-
window.removeEventListener('focus', onFocus);
|
|
498
|
-
}, 500); // Esperamos un poco para asegurarnos de que onchange haya tenido oportunidad de ejecutarse
|
|
499
|
-
};
|
|
500
|
-
window.addEventListener('focus', onFocus);
|
|
501
|
-
this.fileInput.accept = options?.accept ? options.accept : '';
|
|
502
|
-
this.fileInput.multiple = options?.multiple || false;
|
|
503
|
-
this.fileInput.click();
|
|
504
|
-
if (callback) {
|
|
505
|
-
this.callback = callback;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FilesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
509
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FilesService, providedIn: 'root' });
|
|
510
|
-
}
|
|
511
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FilesService, decorators: [{
|
|
588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DateUtil, decorators: [{
|
|
512
589
|
type: Injectable,
|
|
513
590
|
args: [{
|
|
514
591
|
providedIn: 'root',
|
|
515
592
|
}]
|
|
516
|
-
}]
|
|
593
|
+
}] });
|
|
517
594
|
|
|
518
|
-
|
|
519
|
-
|
|
595
|
+
/**
|
|
596
|
+
* Service for opening file picker dialogs and reading file contents.
|
|
597
|
+
*
|
|
598
|
+
* @remarks
|
|
599
|
+
* Provides a programmatic way to open file selection dialogs and read file data as base64.
|
|
600
|
+
*/
|
|
601
|
+
class FilePicker {
|
|
602
|
+
/**
|
|
603
|
+
* Opens a file picker dialog and returns the selected files with their data as base64.
|
|
604
|
+
*
|
|
605
|
+
* @param options - Optional configuration for the file picker
|
|
606
|
+
* @param options.accept - File types to accept (e.g., 'image/*', '.pdf')
|
|
607
|
+
* @param options.multiple - Whether to allow multiple file selection
|
|
608
|
+
* @returns Promise that resolves to an array of files with data property, or null if cancelled
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```typescript
|
|
612
|
+
* const files = await filePicker.open({ accept: 'image/*', multiple: true });
|
|
613
|
+
* if (files) {
|
|
614
|
+
* files.forEach(file => console.log(file.data)); // base64 data
|
|
615
|
+
* }
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
520
618
|
async open(options) {
|
|
521
619
|
return new Promise((resolve, reject) => {
|
|
522
620
|
const fileInput = document.createElement('input');
|
|
523
621
|
fileInput.type = 'file';
|
|
524
|
-
fileInput.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}));
|
|
537
|
-
if (data.length == files.length) {
|
|
538
|
-
fileInput.value = '';
|
|
539
|
-
resolve(data.length > 0 ? data : null);
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
resolve(null);
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
}
|
|
622
|
+
fileInput.style.display = 'none';
|
|
623
|
+
if (options?.accept) {
|
|
624
|
+
fileInput.accept = options.accept;
|
|
625
|
+
}
|
|
626
|
+
fileInput.multiple = options?.multiple ?? false;
|
|
627
|
+
document.body.appendChild(fileInput);
|
|
628
|
+
let changeHandled = false;
|
|
629
|
+
const cleanUp = () => {
|
|
630
|
+
window.removeEventListener('focus', onFocus);
|
|
631
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
632
|
+
if (fileInput.parentNode) {
|
|
633
|
+
document.body.removeChild(fileInput);
|
|
546
634
|
}
|
|
547
|
-
}
|
|
548
|
-
this.pickerClosed = false;
|
|
549
|
-
// Detectar cuando el usuario vuelve a la ventana después de abrir el picker
|
|
635
|
+
};
|
|
550
636
|
const onFocus = () => {
|
|
551
637
|
setTimeout(() => {
|
|
552
|
-
if (!
|
|
553
|
-
|
|
638
|
+
if (!changeHandled) {
|
|
639
|
+
cleanUp();
|
|
640
|
+
resolve(null);
|
|
554
641
|
}
|
|
555
|
-
|
|
556
|
-
}, 500); // Esperamos un poco para asegurarnos de que onchange haya tenido oportunidad de ejecutarse
|
|
642
|
+
}, 1500);
|
|
557
643
|
};
|
|
644
|
+
const onVisibilityChange = () => {
|
|
645
|
+
if (document.visibilityState === 'visible') {
|
|
646
|
+
setTimeout(() => {
|
|
647
|
+
if (!changeHandled) {
|
|
648
|
+
cleanUp();
|
|
649
|
+
resolve(null);
|
|
650
|
+
}
|
|
651
|
+
}, 1500);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
fileInput.addEventListener('change', async (event) => {
|
|
655
|
+
changeHandled = true;
|
|
656
|
+
const files = Array.from(event.target.files || []);
|
|
657
|
+
if (!files.length) {
|
|
658
|
+
cleanUp();
|
|
659
|
+
resolve(null);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const result = await Promise.all(files.map(file => {
|
|
664
|
+
return new Promise((res, rej) => {
|
|
665
|
+
const reader = new FileReader();
|
|
666
|
+
reader.onload = () => {
|
|
667
|
+
res(Object.assign(file, { data: reader.result }));
|
|
668
|
+
};
|
|
669
|
+
reader.onerror = (err) => {
|
|
670
|
+
rej(reader.error);
|
|
671
|
+
};
|
|
672
|
+
reader.readAsDataURL(file);
|
|
673
|
+
});
|
|
674
|
+
}));
|
|
675
|
+
cleanUp();
|
|
676
|
+
resolve(result);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
cleanUp();
|
|
680
|
+
reject(error);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
558
683
|
window.addEventListener('focus', onFocus);
|
|
559
|
-
|
|
560
|
-
fileInput.multiple = options?.multiple || false;
|
|
684
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
561
685
|
fileInput.click();
|
|
562
686
|
});
|
|
563
687
|
}
|
|
564
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
565
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
688
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FilePicker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
689
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FilePicker, providedIn: 'root' });
|
|
566
690
|
}
|
|
567
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
691
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FilePicker, decorators: [{
|
|
568
692
|
type: Injectable,
|
|
569
693
|
args: [{
|
|
570
|
-
providedIn: 'root'
|
|
694
|
+
providedIn: 'root',
|
|
571
695
|
}]
|
|
572
696
|
}] });
|
|
573
697
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
'https://www.googletagmanager.com/gtm.js?id='+i+dl+p;f.parentNode.insertBefore(j,f);
|
|
594
|
-
})(window,document,'script','dataLayer','${trackingId}');
|
|
595
|
-
`;
|
|
596
|
-
document.head.appendChild(s1);
|
|
597
|
-
const s2 = document.createElement('noscript');
|
|
598
|
-
const s3 = document.createElement('iframe');
|
|
599
|
-
s3.width = '0';
|
|
600
|
-
s3.height = '0';
|
|
601
|
-
s3.style.display = 'none';
|
|
602
|
-
s3.style.visibility = 'hidden';
|
|
603
|
-
s3.src = `//www.googletagmanager.com/ns.html?id=${trackingId}`;
|
|
604
|
-
s2.appendChild(s3);
|
|
605
|
-
document.body.prepend(s2);
|
|
606
|
-
this.initSubscribers();
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch (ex) {
|
|
610
|
-
console.error('Error appending google tag manager');
|
|
611
|
-
console.error(ex);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
addVariable(variable) {
|
|
615
|
-
if (isPlatformBrowser(this.platformId) && this.trackingId) {
|
|
616
|
-
window.dataLayer = window.dataLayer || [];
|
|
617
|
-
window.dataLayer.push(variable);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
initSubscribers() {
|
|
621
|
-
this.router.events.subscribe(event => {
|
|
622
|
-
try {
|
|
623
|
-
if (event instanceof NavigationEnd && this.trackingId) {
|
|
624
|
-
this.addVariable({
|
|
625
|
-
event: 'router.NavigationEnd',
|
|
626
|
-
pageTitle: document.title,
|
|
627
|
-
pagePath: event.urlAfterRedirects
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
catch (e) {
|
|
632
|
-
console.error(e);
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GoogleTagManagerService, deps: [{ token: PLATFORM_ID }, { token: i1.Router }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
637
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GoogleTagManagerService, providedIn: 'root' });
|
|
638
|
-
}
|
|
639
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GoogleTagManagerService, decorators: [{
|
|
640
|
-
type: Injectable,
|
|
641
|
-
args: [{
|
|
642
|
-
providedIn: 'root'
|
|
643
|
-
}]
|
|
644
|
-
}], ctorParameters: () => [{ type: Object, decorators: [{
|
|
645
|
-
type: Inject,
|
|
646
|
-
args: [PLATFORM_ID]
|
|
647
|
-
}] }, { type: i1.Router }] });
|
|
648
|
-
|
|
649
|
-
class ObjectService {
|
|
698
|
+
/**
|
|
699
|
+
* Utility service for object operations.
|
|
700
|
+
*
|
|
701
|
+
* @remarks
|
|
702
|
+
* Provides helper methods for object manipulation and transformation.
|
|
703
|
+
*/
|
|
704
|
+
class ObjectUtil {
|
|
705
|
+
/**
|
|
706
|
+
* Filters out null, undefined, and 'undefined' string properties from an object.
|
|
707
|
+
*
|
|
708
|
+
* @param obj - The object to filter
|
|
709
|
+
* @returns A new object with null/undefined properties removed
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```typescript
|
|
713
|
+
* const filtered = objectUtil.filterNullProperties({ a: 1, b: null, c: undefined });
|
|
714
|
+
* // Returns: { a: 1 }
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
650
717
|
filterNullProperties(obj) {
|
|
651
718
|
const mappedObj = {};
|
|
652
719
|
Object.keys(obj).forEach((key) => {
|
|
@@ -656,6 +723,22 @@ class ObjectService {
|
|
|
656
723
|
});
|
|
657
724
|
return mappedObj;
|
|
658
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Performs a deep merge of two objects, combining nested properties.
|
|
728
|
+
*
|
|
729
|
+
* @param target - The target object to merge into
|
|
730
|
+
* @param source - The source object to merge from
|
|
731
|
+
* @returns A new object with deeply merged properties
|
|
732
|
+
*
|
|
733
|
+
* @remarks
|
|
734
|
+
* Uses structuredClone to avoid mutating the original objects.
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* const merged = objectUtil.deepMerge({ a: { b: 1 } }, { a: { c: 2 } });
|
|
739
|
+
* // Returns: { a: { b: 1, c: 2 } }
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
659
742
|
deepMerge(target, source) {
|
|
660
743
|
source = structuredClone(source);
|
|
661
744
|
target = structuredClone(target);
|
|
@@ -666,23 +749,34 @@ class ObjectService {
|
|
|
666
749
|
}
|
|
667
750
|
return { ...target, ...source };
|
|
668
751
|
}
|
|
669
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
670
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
752
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ObjectUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
753
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ObjectUtil, providedIn: 'root' });
|
|
671
754
|
}
|
|
672
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
755
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ObjectUtil, decorators: [{
|
|
673
756
|
type: Injectable,
|
|
674
757
|
args: [{
|
|
675
|
-
providedIn: 'root'
|
|
758
|
+
providedIn: 'root',
|
|
676
759
|
}]
|
|
677
760
|
}] });
|
|
678
761
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Service for managing browser storage (localStorage, sessionStorage, and memory storage).
|
|
764
|
+
*
|
|
765
|
+
* @remarks
|
|
766
|
+
* Provides a unified interface for storing and retrieving data from different storage types.
|
|
767
|
+
* Automatically handles JSON serialization/deserialization and platform detection.
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* ```typescript
|
|
771
|
+
* const storage = inject(Storage);
|
|
772
|
+
* storage.set('key', { data: 'value' }, 'local');
|
|
773
|
+
* const value = storage.get('key', 'local');
|
|
774
|
+
* ```
|
|
775
|
+
*/
|
|
776
|
+
class Storage {
|
|
777
|
+
// TODO: Replace with Map object it is more efficient
|
|
778
|
+
memoryStorage = {};
|
|
779
|
+
platformId = inject(PLATFORM_ID);
|
|
686
780
|
getValue(key, storage) {
|
|
687
781
|
let value;
|
|
688
782
|
if (!storage || typeof storage == 'string') {
|
|
@@ -703,6 +797,12 @@ class StorageService {
|
|
|
703
797
|
}
|
|
704
798
|
return value;
|
|
705
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Deletes a value from the specified storage.
|
|
802
|
+
*
|
|
803
|
+
* @param key - The key of the value to delete
|
|
804
|
+
* @param storage - The storage type ('local', 'session', or 'memory'). Defaults to 'session'
|
|
805
|
+
*/
|
|
706
806
|
delete(key, storage) {
|
|
707
807
|
if (isPlatformBrowser(this.platformId)) {
|
|
708
808
|
if (!storage || typeof storage == 'string') {
|
|
@@ -723,6 +823,16 @@ class StorageService {
|
|
|
723
823
|
}
|
|
724
824
|
}
|
|
725
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Retrieves a value from the specified storage.
|
|
828
|
+
*
|
|
829
|
+
* @param key - The key of the value to retrieve
|
|
830
|
+
* @param storage - The storage type ('local', 'session', or 'memory'). Defaults to 'session'
|
|
831
|
+
* @returns The parsed value (if JSON) or the raw value, or undefined if not found
|
|
832
|
+
*
|
|
833
|
+
* @remarks
|
|
834
|
+
* Automatically attempts to parse JSON values. If parsing fails, returns the raw string.
|
|
835
|
+
*/
|
|
726
836
|
get(key, storage) {
|
|
727
837
|
let parsedValue;
|
|
728
838
|
if (isPlatformBrowser(this.platformId)) {
|
|
@@ -735,6 +845,16 @@ class StorageService {
|
|
|
735
845
|
}
|
|
736
846
|
return parsedValue;
|
|
737
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Stores a value in the specified storage.
|
|
850
|
+
*
|
|
851
|
+
* @param key - The key to store the value under
|
|
852
|
+
* @param value - The value to store (will be JSON stringified)
|
|
853
|
+
* @param storage - The storage type ('local', 'session', or 'memory'). Defaults to 'session'
|
|
854
|
+
*
|
|
855
|
+
* @remarks
|
|
856
|
+
* Values are automatically JSON stringified before storage.
|
|
857
|
+
*/
|
|
738
858
|
set(key, value, storage) {
|
|
739
859
|
if (isPlatformBrowser(this.platformId)) {
|
|
740
860
|
const valueString = JSON.stringify(value);
|
|
@@ -753,20 +873,36 @@ class StorageService {
|
|
|
753
873
|
}
|
|
754
874
|
}
|
|
755
875
|
}
|
|
756
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
757
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
876
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Storage, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
877
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Storage, providedIn: 'root' });
|
|
758
878
|
}
|
|
759
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
879
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Storage, decorators: [{
|
|
760
880
|
type: Injectable,
|
|
761
881
|
args: [{
|
|
762
882
|
providedIn: 'root',
|
|
763
883
|
}]
|
|
764
|
-
}]
|
|
765
|
-
type: Inject,
|
|
766
|
-
args: [PLATFORM_ID]
|
|
767
|
-
}] }] });
|
|
884
|
+
}] });
|
|
768
885
|
|
|
769
|
-
|
|
886
|
+
/**
|
|
887
|
+
* Utility service for string operations.
|
|
888
|
+
*
|
|
889
|
+
* @remarks
|
|
890
|
+
* Provides helper methods for common string manipulation and formatting tasks.
|
|
891
|
+
*/
|
|
892
|
+
class StringUtil {
|
|
893
|
+
/**
|
|
894
|
+
* Decodes HTML entities in a string.
|
|
895
|
+
*
|
|
896
|
+
* @param text - The string containing HTML entities to decode
|
|
897
|
+
* @returns The decoded string
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* ```typescript
|
|
901
|
+
* const encoded = '<div>Hello</div>';
|
|
902
|
+
* const decoded = stringUtil.decodeHTML(encoded);
|
|
903
|
+
* // Returns: '<div>Hello</div>'
|
|
904
|
+
* ```
|
|
905
|
+
*/
|
|
770
906
|
decodeHTML(text) {
|
|
771
907
|
const span = document.createElement('span');
|
|
772
908
|
return text.replace(/&[#A-Za-z0-9]+;/gi, (entity) => {
|
|
@@ -775,9 +911,16 @@ class StringService {
|
|
|
775
911
|
});
|
|
776
912
|
}
|
|
777
913
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
* @
|
|
914
|
+
* Normalizes a text by converting to lowercase and removing accents.
|
|
915
|
+
*
|
|
916
|
+
* @param text - The text to normalize
|
|
917
|
+
* @returns The normalized text without accents and in lowercase
|
|
918
|
+
*
|
|
919
|
+
* @example
|
|
920
|
+
* ```typescript
|
|
921
|
+
* const normalized = stringUtil.normalize('Café');
|
|
922
|
+
* // Returns: 'cafe'
|
|
923
|
+
* ```
|
|
781
924
|
*/
|
|
782
925
|
normalize(text) {
|
|
783
926
|
if (!text)
|
|
@@ -787,37 +930,179 @@ class StringService {
|
|
|
787
930
|
.normalize("NFD")
|
|
788
931
|
.replace(/[\u0300-\u036f]/g, "");
|
|
789
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Normalizes a name by trimming whitespace and capitalizing the first letter.
|
|
935
|
+
*
|
|
936
|
+
* @param text - The name text to normalize
|
|
937
|
+
* @returns The normalized name with first letter capitalized
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* ```typescript
|
|
941
|
+
* const normalized = stringUtil.normalizeName(' john doe ');
|
|
942
|
+
* // Returns: 'John doe'
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
790
945
|
normalizeName(text) {
|
|
791
946
|
text = text.trim();
|
|
792
947
|
text = text.charAt(0).toUpperCase() + text.slice(1);
|
|
793
948
|
return text;
|
|
794
949
|
}
|
|
795
950
|
/**
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* @
|
|
951
|
+
* Calculates the estimated reading time in milliseconds for a given text.
|
|
952
|
+
*
|
|
953
|
+
* @param text - The text to calculate reading time for
|
|
954
|
+
* @returns The estimated reading time in milliseconds
|
|
955
|
+
*
|
|
956
|
+
* @remarks
|
|
957
|
+
* Uses a reading speed of 1200 characters per minute.
|
|
958
|
+
*
|
|
959
|
+
* @example
|
|
960
|
+
* ```typescript
|
|
961
|
+
* const readingTime = stringUtil.calculateReadingTime('Long text here...');
|
|
962
|
+
* // Returns: estimated milliseconds
|
|
963
|
+
* ```
|
|
799
964
|
*/
|
|
800
965
|
calculateReadingTime(text) {
|
|
801
|
-
//
|
|
966
|
+
// Characters per minute
|
|
802
967
|
const cpm = 1200;
|
|
803
|
-
//
|
|
968
|
+
// Calculate the number of characters in the text
|
|
804
969
|
const numCharacters = text.length;
|
|
805
|
-
//
|
|
970
|
+
// Calculate reading time in minutes
|
|
806
971
|
const readingTimeMinutes = numCharacters / cpm;
|
|
807
|
-
//
|
|
972
|
+
// Convert reading time from minutes to milliseconds
|
|
808
973
|
const readingTimeMilliseconds = readingTimeMinutes * 60 * 1000;
|
|
809
974
|
return readingTimeMilliseconds;
|
|
810
975
|
}
|
|
811
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
812
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
976
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StringUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
977
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StringUtil, providedIn: 'root' });
|
|
813
978
|
}
|
|
814
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
979
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StringUtil, decorators: [{
|
|
815
980
|
type: Injectable,
|
|
816
981
|
args: [{
|
|
817
|
-
providedIn: 'root'
|
|
982
|
+
providedIn: 'root',
|
|
983
|
+
}]
|
|
984
|
+
}] });
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Service for integrating Google Tag Manager (GTM) tracking.
|
|
988
|
+
*
|
|
989
|
+
* @remarks
|
|
990
|
+
* Handles GTM script injection, variable tracking, and automatic route change tracking.
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* ```typescript
|
|
994
|
+
* const gtm = inject(GoogleTagManager);
|
|
995
|
+
* gtm.appendTrackingCode('GTM-XXXXXXX');
|
|
996
|
+
* gtm.addVariable({ event: 'customEvent', data: 'value' });
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
class GoogleTagManager {
|
|
1000
|
+
trackingId;
|
|
1001
|
+
platformId = inject(PLATFORM_ID);
|
|
1002
|
+
router = inject(Router);
|
|
1003
|
+
/**
|
|
1004
|
+
* Appends Google Tag Manager tracking code to the page.
|
|
1005
|
+
*
|
|
1006
|
+
* @param trackingId - The GTM container ID (e.g., 'GTM-XXXXXXX')
|
|
1007
|
+
* @param options - Optional configuration for GTM environment
|
|
1008
|
+
* @param options.environment - Environment configuration for preview mode
|
|
1009
|
+
* @param options.environment.auth - Authentication token for preview
|
|
1010
|
+
* @param options.environment.preview - Preview container ID
|
|
1011
|
+
*
|
|
1012
|
+
* @remarks
|
|
1013
|
+
* Automatically subscribes to router navigation events for page view tracking.
|
|
1014
|
+
*/
|
|
1015
|
+
appendTrackingCode(trackingId, options) {
|
|
1016
|
+
try {
|
|
1017
|
+
if (isPlatformBrowser(this.platformId) && trackingId) {
|
|
1018
|
+
this.trackingId = trackingId;
|
|
1019
|
+
const s1 = document.createElement('script');
|
|
1020
|
+
s1.innerHTML = `
|
|
1021
|
+
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
1022
|
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
1023
|
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'',p='${options?.environment?.preview
|
|
1024
|
+
? '>m_auth=' + options?.environment.auth + '>m_preview=' + options?.environment.preview
|
|
1025
|
+
: ''}';j.async=true;j.src=
|
|
1026
|
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl+p;f.parentNode.insertBefore(j,f);
|
|
1027
|
+
})(window,document,'script','dataLayer','${trackingId}');
|
|
1028
|
+
`;
|
|
1029
|
+
document.head.appendChild(s1);
|
|
1030
|
+
const s2 = document.createElement('noscript');
|
|
1031
|
+
const s3 = document.createElement('iframe');
|
|
1032
|
+
s3.width = '0';
|
|
1033
|
+
s3.height = '0';
|
|
1034
|
+
s3.style.display = 'none';
|
|
1035
|
+
s3.style.visibility = 'hidden';
|
|
1036
|
+
s3.src = `//www.googletagmanager.com/ns.html?id=${trackingId}`;
|
|
1037
|
+
s2.appendChild(s3);
|
|
1038
|
+
document.body.prepend(s2);
|
|
1039
|
+
this.initSubscribers();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
catch (ex) {
|
|
1043
|
+
console.error('Error appending google tag manager');
|
|
1044
|
+
console.error(ex);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Pushes a variable or event to the GTM dataLayer.
|
|
1049
|
+
*
|
|
1050
|
+
* @param variable - The variable or event object to push to dataLayer
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```typescript
|
|
1054
|
+
* gtm.addVariable({ event: 'purchase', value: 100 });
|
|
1055
|
+
* ```
|
|
1056
|
+
*/
|
|
1057
|
+
addVariable(variable) {
|
|
1058
|
+
if (isPlatformBrowser(this.platformId) && this.trackingId) {
|
|
1059
|
+
window.dataLayer = window.dataLayer || [];
|
|
1060
|
+
window.dataLayer.push(variable);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Initializes router event subscribers for automatic page view tracking.
|
|
1065
|
+
*
|
|
1066
|
+
* @private
|
|
1067
|
+
*/
|
|
1068
|
+
initSubscribers() {
|
|
1069
|
+
this.router.events.subscribe(event => {
|
|
1070
|
+
try {
|
|
1071
|
+
if (event instanceof NavigationEnd && this.trackingId) {
|
|
1072
|
+
this.addVariable({
|
|
1073
|
+
event: 'router.NavigationEnd',
|
|
1074
|
+
pageTitle: document.title,
|
|
1075
|
+
pagePath: event.urlAfterRedirects
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
|
+
console.error(e);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GoogleTagManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1085
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GoogleTagManager, providedIn: 'root' });
|
|
1086
|
+
}
|
|
1087
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GoogleTagManager, decorators: [{
|
|
1088
|
+
type: Injectable,
|
|
1089
|
+
args: [{
|
|
1090
|
+
providedIn: 'root',
|
|
818
1091
|
}]
|
|
819
1092
|
}] });
|
|
820
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* Constants for CRUD operation types.
|
|
1096
|
+
*/
|
|
1097
|
+
const OPERATION_TYPE = {
|
|
1098
|
+
/** Create operation */
|
|
1099
|
+
CREATE: 'create',
|
|
1100
|
+
/** Update operation */
|
|
1101
|
+
UPDATE: 'update',
|
|
1102
|
+
/** Delete operation */
|
|
1103
|
+
DELETE: 'delete'
|
|
1104
|
+
};
|
|
1105
|
+
|
|
821
1106
|
function lengthValidator(number, digits) {
|
|
822
1107
|
let value = true;
|
|
823
1108
|
if (number.trim() === '') { // No puede estar vacio
|
|
@@ -938,6 +1223,18 @@ function storeCodeValidator(number) {
|
|
|
938
1223
|
}
|
|
939
1224
|
return value;
|
|
940
1225
|
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Determines the type of Ecuadorian identification number.
|
|
1228
|
+
*
|
|
1229
|
+
* @param number - The identification number to analyze
|
|
1230
|
+
* @returns The identification type ('cedula', 'ruc_natural', 'ruc_privada', 'ruc_publica') or null if invalid
|
|
1231
|
+
*
|
|
1232
|
+
* @example
|
|
1233
|
+
* ```typescript
|
|
1234
|
+
* const type = getIdentificationType('1234567890');
|
|
1235
|
+
* // Returns: 'cedula' or null
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
941
1238
|
function getIdentificationType(number) {
|
|
942
1239
|
let type = null;
|
|
943
1240
|
if (lengthValidator(number, 10) &&
|
|
@@ -972,6 +1269,23 @@ function getIdentificationType(number) {
|
|
|
972
1269
|
return type;
|
|
973
1270
|
}
|
|
974
1271
|
|
|
1272
|
+
/**
|
|
1273
|
+
* Creates a validator function for Ecuadorian identification numbers (cédula or RUC).
|
|
1274
|
+
*
|
|
1275
|
+
* @param type - The type of identification to validate
|
|
1276
|
+
* - 'cedula': Validates cédula (10 digits)
|
|
1277
|
+
* - 'ruc_natural': Validates RUC for natural persons (13 digits)
|
|
1278
|
+
* - 'ruc_privada': Validates RUC for private companies (13 digits)
|
|
1279
|
+
* - 'ruc_publica': Validates RUC for public companies (13 digits)
|
|
1280
|
+
* - 'ruc': Validates any type of RUC
|
|
1281
|
+
* - 'id': Validates any valid identification type
|
|
1282
|
+
* @returns A validator function that returns an error object if validation fails
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
* ```typescript
|
|
1286
|
+
* const control = new FormControl('', [identificationValidator('cedula')]);
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
975
1289
|
function identificationValidator(type) {
|
|
976
1290
|
return (control) => {
|
|
977
1291
|
const number = String(control.value);
|
|
@@ -1001,9 +1315,440 @@ function identificationValidator(type) {
|
|
|
1001
1315
|
};
|
|
1002
1316
|
}
|
|
1003
1317
|
|
|
1318
|
+
class AuthProvider {
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Injection token for auth configuration.
|
|
1323
|
+
* Provide this in app.config with values from environment.
|
|
1324
|
+
*
|
|
1325
|
+
* @example
|
|
1326
|
+
* ```typescript
|
|
1327
|
+
* import { AUTH_CONFIG } from 'auth-core';
|
|
1328
|
+
* import { environment } from '@/environments/environment';
|
|
1329
|
+
*
|
|
1330
|
+
* providers: [
|
|
1331
|
+
* { provide: AUTH_CONFIG, useValue: environment }
|
|
1332
|
+
* ]
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
const AUTH_CONFIG = new InjectionToken('AUTH_CONFIG');
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Concrete authentication provider responsible for handling session lifecycle,
|
|
1339
|
+
* secure token refresh, and profile management concerns across the app.
|
|
1340
|
+
*
|
|
1341
|
+
* @remarks
|
|
1342
|
+
* The service extends {@link AuthProvider} to leverage core session helpers while
|
|
1343
|
+
* adding stateful logic for dialogs, social login, and settings synchronization.
|
|
1344
|
+
*/
|
|
1345
|
+
class AuthService extends AuthProvider {
|
|
1346
|
+
// Dependency injection
|
|
1347
|
+
config = inject(AUTH_CONFIG);
|
|
1348
|
+
dialog = inject(MatDialog);
|
|
1349
|
+
httpClient = inject(HttpClient);
|
|
1350
|
+
_user = signal(null, ...(ngDevMode ? [{ debugName: "_user" }] : /* istanbul ignore next */ []));
|
|
1351
|
+
/** Session token key */
|
|
1352
|
+
TOKEN_KEY = `${this.config.sessionPrefix}_sess`;
|
|
1353
|
+
user = computed(() => this._user(), ...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
|
|
1354
|
+
constructor() {
|
|
1355
|
+
super();
|
|
1356
|
+
if (this.isLoggedIn()) {
|
|
1357
|
+
this._user.set(this.extractUserFromToken(this.getToken()?.token ?? ''));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Flag indicating whether the access token is being refreshed
|
|
1362
|
+
*/
|
|
1363
|
+
refreshTokenInProgress = false;
|
|
1364
|
+
/**
|
|
1365
|
+
* Manages the access token refresh flow
|
|
1366
|
+
*/
|
|
1367
|
+
refreshTokenSubject = new BehaviorSubject(null);
|
|
1368
|
+
/**
|
|
1369
|
+
* Sends the authentication token to the server
|
|
1370
|
+
* @param request HTTP request
|
|
1371
|
+
* @returns
|
|
1372
|
+
*/
|
|
1373
|
+
addAuthenticationToken(request) {
|
|
1374
|
+
const token = this.getToken();
|
|
1375
|
+
// If the access token is null, the user is not logged in; return the original request
|
|
1376
|
+
if (!token ||
|
|
1377
|
+
request.url.includes(this.config.auth.signinUrl) ||
|
|
1378
|
+
(this.config.auth.refreshTokenUrl && request.url.includes(this.config.auth.refreshTokenUrl))) {
|
|
1379
|
+
return request;
|
|
1380
|
+
}
|
|
1381
|
+
// Clone the request, because the original request is immutable
|
|
1382
|
+
return request.clone({
|
|
1383
|
+
setHeaders: {
|
|
1384
|
+
Authorization: `Bearer ${token.token}`
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async connect(client) {
|
|
1389
|
+
const isChrome = navigator.userAgentData?.brands?.some((b) => b.brand === 'Google Chrome') ?? false;
|
|
1390
|
+
if (this.config.fedcm &&
|
|
1391
|
+
isChrome &&
|
|
1392
|
+
'credentials' in navigator &&
|
|
1393
|
+
navigator.credentials &&
|
|
1394
|
+
'get' in navigator.credentials) {
|
|
1395
|
+
try {
|
|
1396
|
+
const fedcm = this.config.fedcm[client];
|
|
1397
|
+
if (!fedcm) {
|
|
1398
|
+
throw new Error('No auth client exists');
|
|
1399
|
+
}
|
|
1400
|
+
const credential = await navigator.credentials.get({
|
|
1401
|
+
identity: {
|
|
1402
|
+
mode: 'active',
|
|
1403
|
+
providers: [
|
|
1404
|
+
{
|
|
1405
|
+
configURL: fedcm.configURL,
|
|
1406
|
+
clientId: fedcm.clientId,
|
|
1407
|
+
fields: ['name', 'email', 'picture'],
|
|
1408
|
+
params: {
|
|
1409
|
+
fetch_basic_profile: true,
|
|
1410
|
+
response_type: 'permission id_token',
|
|
1411
|
+
scope: 'email profile openid',
|
|
1412
|
+
include_granted_scopes: true,
|
|
1413
|
+
nonce: 'notprovided'
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
},
|
|
1418
|
+
mediation: 'required'
|
|
1419
|
+
});
|
|
1420
|
+
if (!credential) {
|
|
1421
|
+
throw new Error('No credential obtained');
|
|
1422
|
+
}
|
|
1423
|
+
const fedcmCredential = credential;
|
|
1424
|
+
// Send the ID token to the backend
|
|
1425
|
+
const response = await fetch(fedcm.tokenUrl, {
|
|
1426
|
+
method: 'POST',
|
|
1427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
id_token: JSON.parse(fedcmCredential.token).id_token
|
|
1430
|
+
})
|
|
1431
|
+
});
|
|
1432
|
+
const data = await response.json();
|
|
1433
|
+
this.setToken(data.token);
|
|
1434
|
+
this._user.set(data.user);
|
|
1435
|
+
location.href = this.config.appPath;
|
|
1436
|
+
}
|
|
1437
|
+
catch (e) {
|
|
1438
|
+
console.error('FedCM error: ', e);
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
const url = this.config.auth.clients[client];
|
|
1444
|
+
if (url) {
|
|
1445
|
+
location.href = url;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return true;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Extracts user claims from a JWT token payload.
|
|
1452
|
+
* Maps common JWT claims (sub, email, given_name, family_name, etc.) to the User model.
|
|
1453
|
+
* @param jwtToken The JWT token string
|
|
1454
|
+
* @returns User object or null if parsing fails
|
|
1455
|
+
*/
|
|
1456
|
+
extractUserFromToken(jwtToken) {
|
|
1457
|
+
if (!jwtToken)
|
|
1458
|
+
return null;
|
|
1459
|
+
try {
|
|
1460
|
+
const jwtParts = jwtToken.split('.');
|
|
1461
|
+
if (jwtParts.length !== 3)
|
|
1462
|
+
return null;
|
|
1463
|
+
const payload = JSON.parse(window.atob(jwtParts[1]));
|
|
1464
|
+
const roles = Array.isArray(payload['roles'])
|
|
1465
|
+
? payload['roles']
|
|
1466
|
+
: typeof payload['role'] === 'string'
|
|
1467
|
+
? [payload['role']]
|
|
1468
|
+
: [];
|
|
1469
|
+
return {
|
|
1470
|
+
username: (payload['sub'] ??
|
|
1471
|
+
payload['preferred_username'] ??
|
|
1472
|
+
payload['username'] ??
|
|
1473
|
+
''),
|
|
1474
|
+
roles
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
catch {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Extracts the expiration timestamp from a JWT token
|
|
1483
|
+
* @param jwtToken The JWT token string
|
|
1484
|
+
* @returns The expiration timestamp in seconds (JWT exp format) or undefined if not found/invalid
|
|
1485
|
+
*/
|
|
1486
|
+
extractExpirationFromToken(jwtToken) {
|
|
1487
|
+
if (!jwtToken) {
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
const jwtParts = jwtToken.split('.');
|
|
1492
|
+
if (jwtParts.length === 3) {
|
|
1493
|
+
const payload = JSON.parse(window.atob(jwtParts[1]));
|
|
1494
|
+
if (payload.exp) {
|
|
1495
|
+
// Store timestamp in seconds (JWT exp format)
|
|
1496
|
+
return payload.exp;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
catch {
|
|
1501
|
+
// If JWT parsing fails, return undefined
|
|
1502
|
+
}
|
|
1503
|
+
return undefined;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Handles the flow of refreshing the access token or redirecting to sign-in
|
|
1507
|
+
* @param err HTTP error
|
|
1508
|
+
* @param request HTTP request sent
|
|
1509
|
+
* @param next HTTP handler
|
|
1510
|
+
*/
|
|
1511
|
+
handle401Error(err, request, next) {
|
|
1512
|
+
const token = this.getToken();
|
|
1513
|
+
if (token && token.refresh_token && this.config.auth.refreshTokenUrl) {
|
|
1514
|
+
if (!this.refreshTokenInProgress) {
|
|
1515
|
+
this.refreshTokenInProgress = true;
|
|
1516
|
+
this.refreshTokenSubject.next(null);
|
|
1517
|
+
return this.refreshToken().pipe(switchMap((newToken) => {
|
|
1518
|
+
if (newToken) {
|
|
1519
|
+
this.refreshTokenSubject.next(newToken);
|
|
1520
|
+
return next(this.addAuthenticationToken(request));
|
|
1521
|
+
}
|
|
1522
|
+
// If we don't get a new token, logout.
|
|
1523
|
+
this.logout();
|
|
1524
|
+
return throwError(() => new HttpErrorResponse({
|
|
1525
|
+
error: {},
|
|
1526
|
+
headers: new HttpHeaders(),
|
|
1527
|
+
status: 401,
|
|
1528
|
+
statusText: '',
|
|
1529
|
+
url: undefined
|
|
1530
|
+
}));
|
|
1531
|
+
}), catchError((error) => {
|
|
1532
|
+
// It can't replace the access token; set error status 401 to continue flow
|
|
1533
|
+
return throwError(() => new HttpErrorResponse({
|
|
1534
|
+
error: error.error,
|
|
1535
|
+
headers: error.headers,
|
|
1536
|
+
status: 401,
|
|
1537
|
+
statusText: error.statusText,
|
|
1538
|
+
url: error.url || undefined
|
|
1539
|
+
}));
|
|
1540
|
+
}), share(), finalize(() => {
|
|
1541
|
+
this.refreshTokenInProgress = false;
|
|
1542
|
+
}));
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
return this.refreshTokenSubject.pipe(filter((token) => token != null), take(1), switchMap(() => {
|
|
1546
|
+
return next(this.addAuthenticationToken(request));
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
// No refresh token flow
|
|
1552
|
+
if (this.isLoggedIn()) {
|
|
1553
|
+
this.logout();
|
|
1554
|
+
}
|
|
1555
|
+
return throwError(() => err);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Sends sign-in to the server and obtains the authentication token
|
|
1560
|
+
* @param data Authentication data
|
|
1561
|
+
* @returns
|
|
1562
|
+
*/
|
|
1563
|
+
async login(data) {
|
|
1564
|
+
const token = await lastValueFrom(this.httpClient.post(this.config.auth.signinUrl, data));
|
|
1565
|
+
this.setToken(token);
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Logs out the user
|
|
1570
|
+
*/
|
|
1571
|
+
async logout() {
|
|
1572
|
+
this.dialog.closeAll();
|
|
1573
|
+
this.clearToken();
|
|
1574
|
+
this.clearUser();
|
|
1575
|
+
location.href =
|
|
1576
|
+
window.innerWidth < 1000 ? `${this.config.appPath}/auth` : `${this.config.appPath}/signin`;
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
async signup(data, options) {
|
|
1580
|
+
return lastValueFrom(this.httpClient.post(this.config.auth.signupUrl, data, options));
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* If a refresh token is implemented, send it to obtain a new access token
|
|
1584
|
+
* @returns Access token
|
|
1585
|
+
*/
|
|
1586
|
+
refreshToken() {
|
|
1587
|
+
const token = this.getToken();
|
|
1588
|
+
return this.httpClient
|
|
1589
|
+
.post(this.config.auth.refreshTokenUrl, { refresh_token: token?.refresh_token })
|
|
1590
|
+
.pipe(tap((token) => {
|
|
1591
|
+
this.setToken(token);
|
|
1592
|
+
}), catchError((error) => {
|
|
1593
|
+
this.logout();
|
|
1594
|
+
return throwError(() => new Error(error));
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Reads the session token from the Secure cookie (client-side storage).
|
|
1599
|
+
* @internal
|
|
1600
|
+
*/
|
|
1601
|
+
getToken() {
|
|
1602
|
+
if (typeof document === 'undefined' || !document.cookie)
|
|
1603
|
+
return null;
|
|
1604
|
+
const match = document.cookie.match(new RegExp('(?:^|; )' + this.TOKEN_KEY.replace(/([.*+?^${}()|[\]\\])/g, '\\$1') + '=([^;]*)'));
|
|
1605
|
+
const raw = match ? decodeURIComponent(match[1]) : null;
|
|
1606
|
+
if (!raw)
|
|
1607
|
+
return null;
|
|
1608
|
+
try {
|
|
1609
|
+
const data = JSON.parse(raw);
|
|
1610
|
+
return data?.token ? data : null;
|
|
1611
|
+
}
|
|
1612
|
+
catch {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Sets the authentication token in session state and in a Secure cookie (client-side).
|
|
1618
|
+
* Cookie: Path=/; SameSite=Strict; Secure on HTTPS; Max-Age from expiresAt or 1 day.
|
|
1619
|
+
*
|
|
1620
|
+
* @param token - The authentication token object to store
|
|
1621
|
+
*/
|
|
1622
|
+
setToken(token) {
|
|
1623
|
+
const copy = token ? { ...token } : null;
|
|
1624
|
+
if (copy && typeof document !== 'undefined') {
|
|
1625
|
+
const expiresAt = this.extractExpirationFromToken(copy.token);
|
|
1626
|
+
const value = encodeURIComponent(JSON.stringify(copy));
|
|
1627
|
+
const maxAge = expiresAt
|
|
1628
|
+
? Math.max(0, expiresAt - Math.round(Date.now() / 1000))
|
|
1629
|
+
: 24 * 60 * 60;
|
|
1630
|
+
let cookie = `${this.TOKEN_KEY}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Strict`;
|
|
1631
|
+
if (typeof location !== 'undefined' && location.protocol === 'https:')
|
|
1632
|
+
cookie += '; Secure';
|
|
1633
|
+
document.cookie = cookie;
|
|
1634
|
+
this._user.set(this.extractUserFromToken(copy.token) ?? null);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Clears the authentication token from session state and removes the cookie.
|
|
1639
|
+
*/
|
|
1640
|
+
clearToken() {
|
|
1641
|
+
if (typeof document !== 'undefined') {
|
|
1642
|
+
document.cookie = `${this.TOKEN_KEY}=; Path=/; Max-Age=0`;
|
|
1643
|
+
}
|
|
1644
|
+
this.clearUser();
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Clears the current user from session state
|
|
1648
|
+
*/
|
|
1649
|
+
clearUser() {
|
|
1650
|
+
this._user.set(null);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Computed signal indicating whether a user is currently logged in
|
|
1654
|
+
* Based on the presence and validity of the authentication token
|
|
1655
|
+
*
|
|
1656
|
+
* For JWT tokens, validates expiration. For other token types, only checks existence.
|
|
1657
|
+
*
|
|
1658
|
+
* @returns true if a valid token exists, false otherwise
|
|
1659
|
+
*/
|
|
1660
|
+
isLoggedIn = computed(() => {
|
|
1661
|
+
// Depend on _user so computed re-evaluates when setToken/clearToken updates state
|
|
1662
|
+
this._user();
|
|
1663
|
+
const token = this.getToken();
|
|
1664
|
+
if (!token)
|
|
1665
|
+
return false;
|
|
1666
|
+
const expiresAt = this.extractExpirationFromToken(token.token);
|
|
1667
|
+
if (expiresAt) {
|
|
1668
|
+
const currentTimestamp = Math.round(Date.now() / 1000);
|
|
1669
|
+
return expiresAt > currentTimestamp;
|
|
1670
|
+
}
|
|
1671
|
+
return true;
|
|
1672
|
+
}, ...(ngDevMode ? [{ debugName: "isLoggedIn" }] : /* istanbul ignore next */ []));
|
|
1673
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1674
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, providedIn: 'root' });
|
|
1675
|
+
}
|
|
1676
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, decorators: [{
|
|
1677
|
+
type: Injectable,
|
|
1678
|
+
args: [{
|
|
1679
|
+
providedIn: 'root'
|
|
1680
|
+
}]
|
|
1681
|
+
}], ctorParameters: () => [] });
|
|
1682
|
+
|
|
1683
|
+
const authGuard = async (route, state) => {
|
|
1684
|
+
const config = inject(AUTH_CONFIG);
|
|
1685
|
+
const router = inject(Router);
|
|
1686
|
+
const authProvider = inject(AuthProvider);
|
|
1687
|
+
const storageService = inject(Storage);
|
|
1688
|
+
if (!authProvider.isLoggedIn()) {
|
|
1689
|
+
storageService.set(`${config.sessionPrefix}_rdi`, state.url);
|
|
1690
|
+
return router.createUrlTree([window.innerWidth < 1000 ? '/auth' : '/signin']);
|
|
1691
|
+
}
|
|
1692
|
+
return true;
|
|
1693
|
+
};
|
|
1694
|
+
const loginGuard = () => {
|
|
1695
|
+
const authProvider = inject(AuthProvider);
|
|
1696
|
+
const router = inject(Router);
|
|
1697
|
+
return authProvider.isLoggedIn() ? router.createUrlTree(['/']) : true;
|
|
1698
|
+
};
|
|
1699
|
+
const resetGuard = (route) => {
|
|
1700
|
+
const authProvider = inject(AuthProvider);
|
|
1701
|
+
const router = inject(Router);
|
|
1702
|
+
const token = route.queryParamMap.get('token');
|
|
1703
|
+
if (token && !authProvider.isLoggedIn()) {
|
|
1704
|
+
return true;
|
|
1705
|
+
}
|
|
1706
|
+
else if (authProvider.isLoggedIn()) {
|
|
1707
|
+
return router.createUrlTree(['/']);
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
router.navigateByUrl(`/error/403`, { skipLocationChange: true });
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
const authInterceptor = (req, next) => {
|
|
1716
|
+
const authConfig = inject(AUTH_CONFIG);
|
|
1717
|
+
const authProvider = inject(AuthProvider);
|
|
1718
|
+
const authReq = authProvider.addAuthenticationToken(req);
|
|
1719
|
+
return next(authReq).pipe(catchError((error) => {
|
|
1720
|
+
if (error instanceof HttpErrorResponse) {
|
|
1721
|
+
switch (error.status) {
|
|
1722
|
+
case 401:
|
|
1723
|
+
if (!req.url.includes(authConfig.auth.refreshTokenUrl)) {
|
|
1724
|
+
return authProvider.handle401Error(error, req, next);
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
authProvider.logout();
|
|
1728
|
+
return throwError(() => error);
|
|
1729
|
+
}
|
|
1730
|
+
default:
|
|
1731
|
+
return throwError(() => error);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
else {
|
|
1735
|
+
return throwError(() => error);
|
|
1736
|
+
}
|
|
1737
|
+
}));
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
/*
|
|
1741
|
+
* Public API exports for utils library
|
|
1742
|
+
*/
|
|
1743
|
+
// Services/Utils
|
|
1744
|
+
|
|
1745
|
+
/*
|
|
1746
|
+
* Public API Surface of utils
|
|
1747
|
+
*/
|
|
1748
|
+
|
|
1004
1749
|
/**
|
|
1005
1750
|
* Generated bundle index. Do not edit.
|
|
1006
1751
|
*/
|
|
1007
1752
|
|
|
1008
|
-
export {
|
|
1753
|
+
export { AUTH_CONFIG, ArrayUtil, AuthProvider, AuthService, CSV_CONFIG, ColorUtil, CsvExporter, DateUtil, FilePicker, GoogleTagManager, OPERATION_TYPE, ObjectUtil, Storage, StringUtil, authGuard, authInterceptor, getIdentificationType, identificationValidator, loginGuard, resetGuard };
|
|
1009
1754
|
//# sourceMappingURL=factor_ec-utils.mjs.map
|