@ncim/sdk 0.1.0 → 0.2.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.
@@ -1,6 +1,4123 @@
1
- export * from '@libs/core';
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, provideAppInitializer, effect, inject, DestroyRef, Injectable, signal, computed, Pipe, PLATFORM_ID, Injector, NgZone, ElementRef, HostListener, Directive, afterNextRender, TemplateRef, ViewContainerRef, input, Renderer2 } from '@angular/core';
3
+ import { DOCUMENT, APP_BASE_HREF, isPlatformBrowser } from '@angular/common';
4
+ import { Title, Meta } from '@angular/platform-browser';
5
+ import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
6
+ import { TranslateService, TranslateCompiler } from '@ngx-translate/core';
7
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
+ import { filter } from 'rxjs/operators';
9
+ import { merge, finalize, retry, timer, throwError, tap, map, of, share, catchError, TimeoutError, fromEvent } from 'rxjs';
10
+ import { OAuthService } from 'angular-oauth2-oidc';
11
+ import { HttpContextToken, HttpErrorResponse, HttpClient, HttpContext, HttpParams, HttpResponse, HttpStatusCode } from '@angular/common/http';
12
+ import { PrimeNG } from 'primeng/config';
13
+ import { MessageService } from 'primeng/api';
14
+ import { NgControl } from '@angular/forms';
15
+ import { definePreset } from '@primeuix/themes';
16
+ import Aura from '@primeuix/themes/aura';
17
+
18
+ var EntityType;
19
+ (function (EntityType) {
20
+ EntityType["GOVERNMENT"] = "GOVERNMENT";
21
+ EntityType["INSPECTION"] = "INSPECTION";
22
+ EntityType["BUSINESS"] = "BUSINESS";
23
+ })(EntityType || (EntityType = {}));
24
+
25
+ var PackageType;
26
+ (function (PackageType) {
27
+ PackageType["BASIC"] = "BASIC";
28
+ PackageType["VIOLATIONS"] = "VIOLATIONS";
29
+ PackageType["ENTERPRISE"] = "ENTERPRISE";
30
+ PackageType["CUSTOM"] = "CUSTOM";
31
+ })(PackageType || (PackageType = {}));
32
+
33
+ var ButtonVariant;
34
+ (function (ButtonVariant) {
35
+ ButtonVariant["PRIMARY"] = "primary";
36
+ ButtonVariant["BLACK"] = "black";
37
+ ButtonVariant["NEUTRAL"] = "neutral";
38
+ ButtonVariant["SECONDARY"] = "secondary";
39
+ })(ButtonVariant || (ButtonVariant = {}));
40
+ var ButtonType;
41
+ (function (ButtonType) {
42
+ ButtonType["BUTTON"] = "button";
43
+ ButtonType["SUBMIT"] = "submit";
44
+ })(ButtonType || (ButtonType = {}));
45
+ var ButtonSize;
46
+ (function (ButtonSize) {
47
+ ButtonSize["SMALL"] = "small";
48
+ ButtonSize["MEDIUM"] = "medium";
49
+ ButtonSize["LARGE"] = "large";
50
+ })(ButtonSize || (ButtonSize = {}));
51
+
52
+ const INPUT_TYPES = {
53
+ TEXT: 'text',
54
+ PASSWORD: 'password',
55
+ EMAIL: 'email',
56
+ NUMBER: 'number',
57
+ PHONE: 'phone',
58
+ SELECT: 'select',
59
+ MULTISELECT: 'multiselect',
60
+ TEXTAREA: 'textarea',
61
+ DATE: 'date',
62
+ DATETIME: 'datetime',
63
+ CHECKBOX: 'checkbox',
64
+ RADIO: 'radio',
65
+ CHECKBOX_SEARCH: 'checkbox-search',
66
+ };
67
+
68
+ var LayoutWidth;
69
+ (function (LayoutWidth) {
70
+ LayoutWidth["SMALL"] = "small";
71
+ LayoutWidth["FULL"] = "full";
72
+ })(LayoutWidth || (LayoutWidth = {}));
73
+
74
+ var Language;
75
+ (function (Language) {
76
+ Language["AR"] = "ar";
77
+ Language["EN"] = "en";
78
+ })(Language || (Language = {}));
79
+
80
+ var TrendDirection;
81
+ (function (TrendDirection) {
82
+ TrendDirection["UP"] = "up";
83
+ TrendDirection["DOWN"] = "down";
84
+ })(TrendDirection || (TrendDirection = {}));
85
+
86
+ var MarkerStatus;
87
+ (function (MarkerStatus) {
88
+ MarkerStatus["WARNING"] = "warning";
89
+ MarkerStatus["INFO"] = "info";
90
+ MarkerStatus["ERROR"] = "error";
91
+ MarkerStatus["SUCCESS"] = "success";
92
+ })(MarkerStatus || (MarkerStatus = {}));
93
+
94
+ /** Map legend and card badge status types. Matches legend icons and card status badges. */
95
+ var MapLegendStatus;
96
+ (function (MapLegendStatus) {
97
+ MapLegendStatus["UNCORRECTED_VIOLATION"] = "alert-01";
98
+ MapLegendStatus["UPCOMING_VISIT"] = "briefcase-06";
99
+ MapLegendStatus["CORRECTION_PERIOD"] = "loading";
100
+ MapLegendStatus["LICENSE_EXPIRING"] = "time-quarter-02";
101
+ })(MapLegendStatus || (MapLegendStatus = {}));
102
+
103
+ /** Typed i18n keys for HTTP error toasts. Matches `HTTP_ERRORS.*` in ar.json / en.json. */
104
+ var HttpErrorKey;
105
+ (function (HttpErrorKey) {
106
+ HttpErrorKey["NETWORK"] = "HTTP_ERRORS.NETWORK";
107
+ HttpErrorKey["OFFLINE"] = "HTTP_ERRORS.OFFLINE";
108
+ HttpErrorKey["BACK_ONLINE"] = "HTTP_ERRORS.BACK_ONLINE";
109
+ HttpErrorKey["CORS"] = "HTTP_ERRORS.CORS";
110
+ HttpErrorKey["TIMEOUT"] = "HTTP_ERRORS.TIMEOUT";
111
+ HttpErrorKey["BAD_REQUEST"] = "HTTP_ERRORS.BAD_REQUEST";
112
+ HttpErrorKey["SESSION_EXPIRED"] = "HTTP_ERRORS.SESSION_EXPIRED";
113
+ HttpErrorKey["SESSION_EXPIRED_SSO"] = "HTTP_ERRORS.SESSION_EXPIRED_SSO";
114
+ HttpErrorKey["PAYMENT_REQUIRED"] = "HTTP_ERRORS.PAYMENT_REQUIRED";
115
+ HttpErrorKey["FORBIDDEN"] = "HTTP_ERRORS.FORBIDDEN";
116
+ HttpErrorKey["NOT_FOUND"] = "HTTP_ERRORS.NOT_FOUND";
117
+ HttpErrorKey["METHOD_NOT_ALLOWED"] = "HTTP_ERRORS.METHOD_NOT_ALLOWED";
118
+ HttpErrorKey["NOT_ACCEPTABLE"] = "HTTP_ERRORS.NOT_ACCEPTABLE";
119
+ HttpErrorKey["CONFLICT"] = "HTTP_ERRORS.CONFLICT";
120
+ HttpErrorKey["GONE"] = "HTTP_ERRORS.GONE";
121
+ HttpErrorKey["VALIDATION"] = "HTTP_ERRORS.VALIDATION";
122
+ HttpErrorKey["PAYLOAD_TOO_LARGE"] = "HTTP_ERRORS.PAYLOAD_TOO_LARGE";
123
+ HttpErrorKey["URI_TOO_LONG"] = "HTTP_ERRORS.URI_TOO_LONG";
124
+ HttpErrorKey["UNSUPPORTED_MEDIA_TYPE"] = "HTTP_ERRORS.UNSUPPORTED_MEDIA_TYPE";
125
+ HttpErrorKey["LOCKED"] = "HTTP_ERRORS.LOCKED";
126
+ HttpErrorKey["TOO_MANY_REQUESTS"] = "HTTP_ERRORS.TOO_MANY_REQUESTS";
127
+ HttpErrorKey["RATE_LIMIT_RETRY"] = "HTTP_ERRORS.RATE_LIMIT_RETRY";
128
+ HttpErrorKey["SERVER"] = "HTTP_ERRORS.SERVER";
129
+ HttpErrorKey["NOT_IMPLEMENTED"] = "HTTP_ERRORS.NOT_IMPLEMENTED";
130
+ HttpErrorKey["BAD_GATEWAY"] = "HTTP_ERRORS.BAD_GATEWAY";
131
+ HttpErrorKey["SERVICE_UNAVAILABLE"] = "HTTP_ERRORS.SERVICE_UNAVAILABLE";
132
+ HttpErrorKey["GATEWAY_TIMEOUT"] = "HTTP_ERRORS.GATEWAY_TIMEOUT";
133
+ HttpErrorKey["MAINTENANCE"] = "HTTP_ERRORS.MAINTENANCE";
134
+ HttpErrorKey["UNKNOWN"] = "HTTP_ERRORS.UNKNOWN";
135
+ HttpErrorKey["CHUNK_LOAD"] = "HTTP_ERRORS.CHUNK_LOAD";
136
+ HttpErrorKey["RUNTIME"] = "HTTP_ERRORS.RUNTIME";
137
+ })(HttpErrorKey || (HttpErrorKey = {}));
138
+
139
+ /**
140
+ * All REST API endpoint paths for the Compliance Dashboard.
141
+ *
142
+ * This is the single place to update paths when the real API is integrated.
143
+ * Static paths use `as const`. Dynamic (by-ID) builders are plain functions.
144
+ *
145
+ * Usage:
146
+ * import { API_ENDPOINTS } from './ncim-sdk-core.mjs';
147
+ * this.api.get(API_ENDPOINTS.VIOLATIONS.LIST)
148
+ * this.api.get(API_ENDPOINTS.VIOLATIONS.DETAIL(id))
149
+ */
150
+ const API_ENDPOINTS = {
151
+ AUTH: {
152
+ LOGIN: '/auth/login',
153
+ FORGOT_PASSWORD: '/auth/forgot-password',
154
+ VERIFY_OTP: '/auth/verify-otp',
155
+ RESEND_OTP: '/auth/resend-otp',
156
+ RESET_PASSWORD: '/auth/reset-password',
157
+ REGISTER: '/auth/register',
158
+ REFRESH_TOKEN: '/auth/refresh-token',
159
+ PROFILE_UPDATE: '/auth/profile/update',
160
+ PROFILE_VERIFY_OTP: '/auth/profile/verify-otp',
161
+ PROFILE_RESEND_OTP: '/auth/profile/resend-otp',
162
+ SSO_CALLBACK: '/auth/sso-callback',
163
+ },
164
+ VIOLATIONS: {
165
+ LIST: '/violations/list',
166
+ EXPORT: '/violations/export',
167
+ DETAIL: (id) => `/violations/${id}/details`,
168
+ },
169
+ /** Objections (known as "appeals" on the backend). */
170
+ OBJECTIONS: {
171
+ LIST: '/appeals',
172
+ DETAIL: (id) => `/appeals/${id}`,
173
+ },
174
+ /** Appeals flow — full appeal lifecycle. */
175
+ APPEALS: {
176
+ INITIALIZE: (violationId) => `/appeals/initialize/${violationId}`,
177
+ SUBMIT: '/appeals/submit',
178
+ BY_ID: (appealId) => `/appeals/${appealId}`,
179
+ DETAILS: (appealId) => `/appeals/${appealId}/details`,
180
+ ATTACHMENT_UPLOAD: '/appeals/attachments/upload',
181
+ ATTACHMENT_DOWNLOAD: (fileReference) => `/appeals/attachments/${fileReference}`,
182
+ LIST: '/appeals/list',
183
+ /** GET — returns ObjectionType[] */
184
+ OBJECTION_TYPES: '/appeals/lookups/objection-types',
185
+ },
186
+ USERS: {
187
+ LIST: '/users',
188
+ CREATE: '/users',
189
+ DETAIL: (id) => `/users/${id}`,
190
+ },
191
+ ROLES: {
192
+ LIST: '/roles',
193
+ CREATE: '/roles',
194
+ DETAIL: (id) => `/roles/${id}`,
195
+ },
196
+ SUPPORT: {
197
+ TICKETS: '/support/tickets',
198
+ TICKET: (id) => `/support/tickets/${id}`,
199
+ },
200
+ REPORTS: {
201
+ LIST: '/reports',
202
+ DETAIL: (id) => `/reports/${id}`,
203
+ },
204
+ COMMERCIAL_REGISTERS: {
205
+ LIST: '/commercial-registrations/cards',
206
+ },
207
+ VIEW_LICENSES: {
208
+ GET: (ownerIdentityNo, ownerIdentityType) => `/licenses/${ownerIdentityNo}/${ownerIdentityType}`,
209
+ },
210
+ };
211
+
212
+ const ROUTE_SEGMENTS = {
213
+ AUTH: 'auth',
214
+ LOGIN: 'login',
215
+ FORGOT_PASSWORD: 'forgot-password',
216
+ OTP_VERIFICATION: 'otp-verification',
217
+ RESET_PASSWORD: 'reset-password',
218
+ REGISTER: 'register',
219
+ SUCCESS: 'success',
220
+ DASHBOARD: 'dashboard',
221
+ HOME: 'home',
222
+ REGULATORY_DASHBOARD: 'regulatory-dashboard',
223
+ ENTITY_SECTOR_DETAILS: 'entity-sector-details',
224
+ VIEW_LICENSES: 'view-licenses',
225
+ VIOLATIONS: 'violations',
226
+ OBJECTIONS: 'objections',
227
+ COMPLIANCE_PROGRAM: 'compliance-program',
228
+ SMART_ADVISOR: 'smart-advisor',
229
+ KNOWLEDGE_CENTER: 'knowledge-center',
230
+ KNOWLEDGE_CATEGORY: 'category',
231
+ KNOWLEDGE_TOPIC: 'topic',
232
+ SUPPORTER: 'supporter',
233
+ GRID_DEMO: 'grid-demo',
234
+ DESIGN_SYSTEM: 'components-design-system',
235
+ UI_COMPONENTS_SHOWCASE: 'ui-components',
236
+ ICONS_DOCUMENT: 'icons-document',
237
+ PROFILE: 'profile',
238
+ USERS_AND_ROLES: 'users-and-roles',
239
+ SETTINGS: 'user-management',
240
+ VIOLATION_DETAILS: 'violation-details',
241
+ OBJECTION_DETAILS: 'objection-details',
242
+ SPECIFIC_REPORT_DETAILS: 'report-details',
243
+ LICENSES: 'licenses',
244
+ CHECK_LICENSE: 'check-license',
245
+ SYSTEM_SETTINGS: 'system-settings',
246
+ SSO_CALLBACK: 'sso-callback',
247
+ SSO_ERROR: 'sso-error',
248
+ FORBIDDEN: 'forbidden',
249
+ NOT_FOUND: 'not-found',
250
+ SERVER_ERROR: 'server-error',
251
+ SERVICE_UNAVAILABLE: 'service-unavailable',
252
+ MAINTENANCE: 'maintenance',
253
+ TIMEOUT: 'timeout',
254
+ NO_ROLE: 'no-role',
255
+ MY_FACILITIES: 'my-facilities',
256
+ COMPANY_PROFILE: 'company-profile',
257
+ COMPANY_ONBOARDING: 'company-onboarding',
258
+ ADD_USER_DETAILS: 'add-user-details',
259
+ EDIT_USER_DETAILS: 'edit-user-details',
260
+ EDIT_CONTACT_INFO: 'edit-contact-info',
261
+ LOGIN_BY_NAFATH: 'login-by-nafath',
262
+ LOGIN_BY_NAFATH_VERIFICATION: 'login-by-nafath-verification',
263
+ };
264
+
265
+ const ROUTE_PATHS = {
266
+ HOME: '/',
267
+ AUTH: `/${ROUTE_SEGMENTS.AUTH}`,
268
+ AUTH_LOGIN: `/${ROUTE_SEGMENTS.AUTH}/${ROUTE_SEGMENTS.LOGIN}`,
269
+ AUTH_FORGOT_PASSWORD: `/${ROUTE_SEGMENTS.AUTH}/${ROUTE_SEGMENTS.FORGOT_PASSWORD}`,
270
+ AUTH_OTP_VERIFICATION: `/${ROUTE_SEGMENTS.AUTH}/${ROUTE_SEGMENTS.OTP_VERIFICATION}`,
271
+ AUTH_RESET_PASSWORD: `/${ROUTE_SEGMENTS.AUTH}/${ROUTE_SEGMENTS.RESET_PASSWORD}`,
272
+ REGISTER: `/${ROUTE_SEGMENTS.REGISTER}`,
273
+ REGISTER_SUCCESS: `/${ROUTE_SEGMENTS.REGISTER}/${ROUTE_SEGMENTS.SUCCESS}`,
274
+ DASHBOARD: `/${ROUTE_SEGMENTS.DASHBOARD}`,
275
+ DASHBOARD_HOME: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.HOME}`,
276
+ MY_FACILITIES: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.MY_FACILITIES}`,
277
+ COMPANY_PROFILE: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.COMPANY_PROFILE}`,
278
+ REGULATORY_DASHBOARD: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.REGULATORY_DASHBOARD}`,
279
+ VIOLATIONS: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.VIOLATIONS}`,
280
+ OBJECTIONS: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.OBJECTIONS}`,
281
+ COMPLIANCE_PROGRAM: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.COMPLIANCE_PROGRAM}`,
282
+ SMART_ADVISOR: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.SMART_ADVISOR}`,
283
+ KNOWLEDGE_CENTER: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.KNOWLEDGE_CENTER}`,
284
+ KNOWLEDGE_TOPIC: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.KNOWLEDGE_CENTER}/${ROUTE_SEGMENTS.KNOWLEDGE_TOPIC}`,
285
+ SUPPORTER: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.SUPPORTER}`,
286
+ SETTINGS: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.SETTINGS}`,
287
+ SETTINGS_PROFILE: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.PROFILE}`,
288
+ SETTINGS_USERS_AND_ROLES: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.SETTINGS}/${ROUTE_SEGMENTS.USERS_AND_ROLES}`,
289
+ SETTINGS_SYSTEM: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.SETTINGS}/${ROUTE_SEGMENTS.SYSTEM_SETTINGS}`,
290
+ SSO_CALLBACK: `/${ROUTE_SEGMENTS.SSO_CALLBACK}`,
291
+ SSO_ERROR: `/${ROUTE_SEGMENTS.SSO_ERROR}`,
292
+ FORBIDDEN: `/${ROUTE_SEGMENTS.FORBIDDEN}`,
293
+ NOT_FOUND: `/${ROUTE_SEGMENTS.NOT_FOUND}`,
294
+ SERVER_ERROR: `/${ROUTE_SEGMENTS.SERVER_ERROR}`,
295
+ SERVICE_UNAVAILABLE: `/${ROUTE_SEGMENTS.SERVICE_UNAVAILABLE}`,
296
+ MAINTENANCE: `/${ROUTE_SEGMENTS.MAINTENANCE}`,
297
+ TIMEOUT: `/${ROUTE_SEGMENTS.TIMEOUT}`,
298
+ NO_ROLE: `/${ROUTE_SEGMENTS.NO_ROLE}`,
299
+ ADD_USER_DETAILS: `/${ROUTE_SEGMENTS.ADD_USER_DETAILS}`,
300
+ EDIT_USER_DETAILS: `/${ROUTE_SEGMENTS.EDIT_USER_DETAILS}`,
301
+ EDIT_CONTACT_INFO: `/${ROUTE_SEGMENTS.DASHBOARD}/${ROUTE_SEGMENTS.COMPANY_PROFILE}/${ROUTE_SEGMENTS.EDIT_CONTACT_INFO}`,
302
+ LOGIN_BY_NAFATH_VERIFICATION: `/${ROUTE_SEGMENTS.AUTH}/${ROUTE_SEGMENTS.LOGIN_BY_NAFATH_VERIFICATION}`,
303
+ };
304
+
305
+ /**
306
+ * Centralized regex patterns for validation and directives.
307
+ * Use REGEX for validators/directives; VALIDATION_PATTERNS is the legacy subset for backward compatibility.
308
+ */
309
+ /** All shared regex patterns. */
310
+ const REGEX = {
311
+ // --- Validation (full-string match) ---
312
+ /** Saudi identity: starts with 1 or 2, 10 digits. */
313
+ IDENTITY_NUMBER: /^[12]\d{9}$/,
314
+ /** Saudi mobile: starts with 5, 9 digits. */
315
+ MOBILE_SAUDI: /^[5]\d{8}$/,
316
+ /** Saudi mobile national format: `05` + 8 digits (10 characters). */
317
+ MOBILE_SAUDI_05: /^05\d{8}$/,
318
+ /** Generic 10-digit ID. */
319
+ ID_NUMBER: /^\d{10}$/,
320
+ /** CR number: 10 digits. */
321
+ CR_NUMBER: /^\d{10}$/,
322
+ /** Email address. */
323
+ EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
324
+ /** International phone: optional +, 7–15 digits. */
325
+ PHONE: /^\+?[0-9]{7,15}$/,
326
+ /** HTTP/HTTPS URL. */
327
+ URL: /^https?:\/\/[^\s/$.?#].[^\s]*$/,
328
+ /** One or more digits. */
329
+ NUMBERS_ONLY: /^\d+$/,
330
+ /** Letters (English + Arabic) and spaces. */
331
+ LETTERS_ONLY: /^[a-zA-Z\u0600-\u06FF\s]+$/,
332
+ /** Saudi IBAN: SA + 22 digits. */
333
+ SAUDI_IBAN: /^SA\d{22}$/,
334
+ /** English letters and spaces. */
335
+ ENGLISH_ONLY: /^[a-zA-Z\s]+$/,
336
+ /** Arabic letters and spaces. */
337
+ ARABIC_ONLY: /^[\u0600-\u06FF\s]+$/,
338
+ /** English, Arabic, digits, spaces. */
339
+ ALPHANUMERIC: /^[a-zA-Z0-9\u0600-\u06FF\s]+$/,
340
+ // --- Password strength (character-class tests) ---
341
+ PASSWORD_UPPERCASE: /[A-Z]/,
342
+ PASSWORD_LOWERCASE: /[a-z]/,
343
+ PASSWORD_NUMBER: /\d/,
344
+ PASSWORD_SPECIAL: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/,
345
+ // --- Directives / replace (global) ---
346
+ /** Strip all whitespace. */
347
+ WHITESPACE_GLOBAL: /\s/g,
348
+ /** Strip non-digits (for phone). */
349
+ NON_DIGITS: /[^\d]/g,
350
+ /** Strip non–0-9. */
351
+ NON_NUMBERS: /[^0-9]/g,
352
+ /** Strip non–English letters/spaces. */
353
+ NON_ENGLISH: /[^a-zA-Z\s]/g,
354
+ /** Strip non–email-allowed chars. */
355
+ NON_EMAIL_CHARS: /[^a-zA-Z0-9@._-]/g,
356
+ /** Strip non–Arabic/symbols (Arabic + spaces + Arabic digits). */
357
+ NON_ARABIC: /[^ \u0600-\u06FF\u0660-\u0669]/g,
358
+ /** Strip non–alphanumeric (English, Arabic, digits, space). */
359
+ NON_ALPHANUMERIC: /[^a-zA-Z0-9 \u0600-\u06FF]/g,
360
+ // --- UI / HTML ---
361
+ /** For escaping HTML in attributes/text. */
362
+ HTML_ESCAPE: /[&<>"']/g,
363
+ };
364
+ /** Legacy subset used by validators (identity, Saudi mobile, ID number). */
365
+ const VALIDATION_PATTERNS = {
366
+ IDENTITY_NUMBER: REGEX.IDENTITY_NUMBER,
367
+ MOBILE_SAUDI: REGEX.MOBILE_SAUDI,
368
+ MOBILE_SAUDI_05: REGEX.MOBILE_SAUDI_05,
369
+ ID_NUMBER: REGEX.ID_NUMBER,
370
+ };
371
+
372
+ /** Re-export for backward compatibility. */
373
+ const VALIDATION_LENGTHS = {
374
+ IDENTITY_NUMBER: 10,
375
+ PASSWORD_MIN: 8,
376
+ /** Max digits in national mobile field (with `+966` prefix in UI). `05` + 8 = 10; legacy `5` + 8 = 9 still fits. */
377
+ MOBILE: 10,
378
+ OTP: 4,
379
+ MIN_LENGTH: 3,
380
+ MAX_LENGTH: 255,
381
+ MAX_LENGTH_10: 10,
382
+ MAX_LENGTH_20: 20,
383
+ MAX_LENGTH_30: 30,
384
+ MAX_LENGTH_50: 50,
385
+ MAX_LENGTH_100: 100,
386
+ MAX_LENGTH_255: 255,
387
+ MAX_LENGTH_500: 500,
388
+ };
389
+
390
+ const REGISTRATION_STEPS = {
391
+ FIRST: 1,
392
+ };
393
+ const SIDEBAR_DEFAULT_CONTENT = {
394
+ titleKey: 'AUTH.REGISTER.SIDEBAR_DEFAULT_TITLE',
395
+ descriptionKey: 'AUTH.REGISTER.SIDEBAR_DEFAULT_DESC',
396
+ };
397
+ const SIDEBAR_CONFIG = {
398
+ 1: {
399
+ titleKey: 'AUTH.REGISTER.SIDEBAR_STEP1_TITLE',
400
+ descriptionKey: 'AUTH.REGISTER.SIDEBAR_STEP1_DESC',
401
+ },
402
+ 2: SIDEBAR_DEFAULT_CONTENT,
403
+ 3: SIDEBAR_DEFAULT_CONTENT,
404
+ 4: SIDEBAR_DEFAULT_CONTENT,
405
+ 5: SIDEBAR_DEFAULT_CONTENT,
406
+ };
407
+ const SIDEBAR_DEFAULT_STEP = 2;
408
+ const REGISTRATION_DEFAULTS = {
409
+ REPRESENTATIVE_NAME: 'خالد محمد',
410
+ PACKAGE_NAME: 'الاساسية',
411
+ PACKAGE_PRICE: '23.000',
412
+ MOCK_SESSION_DATE: 'الأثنين، 27 يوليو 2025 - 2:30 مساءً',
413
+ };
414
+ const GOVERNMENT_STEPS = [
415
+ {
416
+ id: '1',
417
+ labelKey: 'AUTH.REGISTER.STEP_ENTITY_TYPE',
418
+ titleKey: 'AUTH.REGISTER.STEP1_TITLE',
419
+ subtitleKey: 'AUTH.REGISTER.STEP1_SUBTITLE',
420
+ },
421
+ {
422
+ id: '2',
423
+ labelKey: 'AUTH.REGISTER.STEP_ENTITY_DETAILS',
424
+ titleKey: 'AUTH.REGISTER.STEP2_TITLE',
425
+ subtitleKey: 'AUTH.REGISTER.STEP2_SUBTITLE',
426
+ },
427
+ {
428
+ id: '3',
429
+ labelKey: 'AUTH.REGISTER.STEP_RESPONSIBLE_USER',
430
+ titleKey: 'AUTH.REGISTER.STEP3_TITLE',
431
+ subtitleKey: 'AUTH.REGISTER.STEP3_SUBTITLE',
432
+ },
433
+ {
434
+ id: '4',
435
+ labelKey: 'AUTH.REGISTER.STEP_PACKAGE',
436
+ titleKey: 'AUTH.REGISTER.STEP4_TITLE',
437
+ subtitleKey: 'AUTH.REGISTER.STEP4_SUBTITLE',
438
+ },
439
+ {
440
+ id: '5',
441
+ labelKey: 'AUTH.REGISTER.STEP_SCHEDULING',
442
+ titleKey: 'AUTH.REGISTER.SESSION_SCHEDULING_TITLE',
443
+ subtitleKey: 'AUTH.REGISTER.SESSION_SCHEDULING_SUBTITLE',
444
+ },
445
+ ];
446
+ const BUSINESS_STEPS = [
447
+ {
448
+ id: '1',
449
+ labelKey: 'AUTH.REGISTER.STEP_ENTITY_TYPE',
450
+ titleKey: 'AUTH.REGISTER.STEP1_TITLE',
451
+ subtitleKey: 'AUTH.REGISTER.STEP1_SUBTITLE',
452
+ },
453
+ {
454
+ id: '2',
455
+ labelKey: 'AUTH.REGISTER.STEP_VERIFICATION',
456
+ titleKey: 'AUTH.REGISTER.CREATE_ACCOUNT_TITLE',
457
+ subtitleKey: 'AUTH.REGISTER.CREATE_ACCOUNT_SUBTITLE',
458
+ },
459
+ ];
460
+ const INSPECTION_STEPS = [
461
+ {
462
+ id: '1',
463
+ labelKey: 'AUTH.REGISTER.STEP_ENTITY_TYPE',
464
+ titleKey: 'AUTH.REGISTER.STEP1_TITLE',
465
+ subtitleKey: 'AUTH.REGISTER.STEP1_SUBTITLE',
466
+ },
467
+ {
468
+ id: '2',
469
+ labelKey: 'AUTH.REGISTER.STEP_ENTITY_DETAILS',
470
+ titleKey: 'AUTH.REGISTER.STEP2_TITLE',
471
+ subtitleKey: 'AUTH.REGISTER.STEP2_SUBTITLE',
472
+ },
473
+ ];
474
+ const ENTITY_TYPE_OPTIONS = [
475
+ {
476
+ id: EntityType.GOVERNMENT,
477
+ title: 'AUTH.REGISTER.ENTITY_GOV',
478
+ description: 'AUTH.REGISTER.ENTITY_GOV_DESC',
479
+ icon: 'building',
480
+ },
481
+ {
482
+ id: EntityType.INSPECTION,
483
+ title: 'AUTH.REGISTER.ENTITY_INSP',
484
+ description: 'AUTH.REGISTER.ENTITY_INSP_DESC',
485
+ icon: 'incognito',
486
+ disabled: true,
487
+ },
488
+ {
489
+ id: EntityType.BUSINESS,
490
+ title: 'AUTH.REGISTER.ENTITY_BUSINESS',
491
+ description: 'AUTH.REGISTER.ENTITY_BUSINESS_DESC',
492
+ icon: 'muslim',
493
+ },
494
+ ];
495
+
496
+ const PACKAGE_FEATURES = [
497
+ { title: 'AUTH.REGISTER.PACKAGE_FEATURE_AI_ADVISOR', included: true },
498
+ { title: 'AUTH.REGISTER.PACKAGE_FEATURE_SELF_ASSESSMENT', included: true },
499
+ { title: 'AUTH.REGISTER.PACKAGE_FEATURE_EDU_MATERIALS', included: true },
500
+ { title: 'AUTH.REGISTER.PACKAGE_FEATURE_ONSITE_VISITS', included: true },
501
+ { title: 'AUTH.REGISTER.PACKAGE_FEATURE_COMMERCIAL_LICENSE', included: true },
502
+ ];
503
+ const PACKAGES = [
504
+ {
505
+ id: PackageType.BASIC,
506
+ name: 'AUTH.REGISTER.BASIC_PACKAGE',
507
+ title: 'AUTH.REGISTER.PACKAGE_FEATURES_TITLE',
508
+ description: 'AUTH.REGISTER.PACKAGE_FEATURES_DESCRIPTION',
509
+ price: '23.000',
510
+ features: PACKAGE_FEATURES.map(feature => feature.title),
511
+ popular: true,
512
+ special: false,
513
+ },
514
+ {
515
+ id: PackageType.VIOLATIONS,
516
+ name: 'AUTH.REGISTER.VIOLATION_DATA',
517
+ title: 'AUTH.REGISTER.PACKAGE_FEATURES_TITLE',
518
+ description: 'AUTH.REGISTER.PACKAGE_FEATURES_DESCRIPTION',
519
+ price: '45.000',
520
+ features: PACKAGE_FEATURES.map(feature => feature.title),
521
+ popular: false,
522
+ special: false,
523
+ },
524
+ {
525
+ id: PackageType.ENTERPRISE,
526
+ name: 'AUTH.REGISTER.ENTERPRISE_PACKAGE',
527
+ title: 'AUTH.REGISTER.PACKAGE_FEATURES_TITLE',
528
+ description: 'AUTH.REGISTER.PACKAGE_FEATURES_DESCRIPTION',
529
+ price: '80.000',
530
+ features: PACKAGE_FEATURES.map(feature => feature.title),
531
+ popular: false,
532
+ special: false,
533
+ },
534
+ {
535
+ id: PackageType.CUSTOM,
536
+ name: 'AUTH.REGISTER.CUSTOM_PACKAGE',
537
+ title: 'AUTH.REGISTER.PACKAGE_CUSTOM_FEATURES_TITLE',
538
+ description: 'AUTH.REGISTER.PACKAGE_FEATURES_DESCRIPTION',
539
+ features: PACKAGE_FEATURES.map(feature => feature.title),
540
+ price: '100.000',
541
+ popular: false,
542
+ special: true,
543
+ },
544
+ ];
545
+ const TIME = [
546
+ '0-2',
547
+ '2-4',
548
+ '4-6',
549
+ '6-8',
550
+ '8-10',
551
+ '10-12',
552
+ '12-14',
553
+ '14-16',
554
+ '16-18',
555
+ '18-20',
556
+ '20-22',
557
+ '22-24',
558
+ ];
559
+
560
+ const ENTITY_DETAILS_OPTIONS = {
561
+ BRANCHES: ['أكثر من 25', '25-11', '6-10', '5-1'],
562
+ EMPLOYEES: ['أكثر من 1000', '1000-500', '250-100', 'أقل من 100'],
563
+ ENTITY_NAMES: [
564
+ { value: 'saudi-water-authority', label: 'الهيئة السعودية للمياه' },
565
+ { value: 'ministry-of-commerce', label: 'وزارة التجارة' },
566
+ { value: 'ministry-of-municipal', label: 'وزارة الشؤون البلدية والقروية والإسكان' },
567
+ { value: 'ministry-of-environment', label: 'وزارة البيئة والمياه والزراعة' },
568
+ { value: 'ministry-of-health', label: 'وزارة الصحة' },
569
+ { value: 'ministry-of-industry', label: 'وزارة الصناعة والثروة المعدنية' },
570
+ { value: 'food-drug-authority', label: 'الهيئة العامة للغذاء والدواء' },
571
+ { value: 'civil-defense', label: 'المديرية العامة للدفاع المدني' },
572
+ { value: 'standards-authority', label: 'الهيئة السعودية للمواصفات والمقاييس' },
573
+ { value: 'energy-authority', label: 'هيئة تنظيم المياه والكهرباء' },
574
+ ],
575
+ REGULATORY_SECTORS: [
576
+ { value: 'environment', label: 'البيئة' },
577
+ { value: 'health', label: 'الصحة' },
578
+ { value: 'commerce', label: 'التجارة' },
579
+ { value: 'industry', label: 'الصناعة' },
580
+ { value: 'food-safety', label: 'سلامة الغذاء' },
581
+ { value: 'fire-safety', label: 'السلامة والحماية من الحريق' },
582
+ { value: 'construction', label: 'البناء والتشييد' },
583
+ { value: 'energy', label: 'الطاقة' },
584
+ { value: 'water', label: 'المياه والصرف الصحي' },
585
+ { value: 'labor', label: 'العمل والتوظيف' },
586
+ ],
587
+ };
588
+ const ENTITY_DETAILS_DEFAULTS = {
589
+ BRANCHES_COUNT: '6-10',
590
+ EMPLOYEES_COUNT: '100-250',
591
+ };
592
+
593
+ const ERROR_KEYS = {
594
+ REQUIRED: 'ERRORS.REQUIRED',
595
+ PASSWORD_MIN_LENGTH: 'ERRORS.PASSWORD_MIN_LENGTH',
596
+ IDENTITY_MIN_LENGTH: 'ERRORS.IDENTITY_MIN_LENGTH',
597
+ IDENTITY_MAX_LENGTH: 'ERRORS.IDENTITY_MAX_LENGTH',
598
+ IDENTITY_INVALID: 'ERRORS.IDENTITY_INVALID',
599
+ IDENTITY_NUMBERS_ONLY: 'ERRORS.IDENTITY_NUMBERS_ONLY',
600
+ MIN_LENGTH: 'ERRORS.MIN_LENGTH',
601
+ MAX_LENGTH: 'ERRORS.MAX_LENGTH',
602
+ MAX_LENGTH_100: 'ERRORS.MAX_LENGTH_100',
603
+ MAX_LENGTH_10: 'ERRORS.MAX_LENGTH_10',
604
+ INVALID_FORMAT: 'ERRORS.INVALID_FORMAT',
605
+ INVALID_EMAIL: 'ERRORS.INVALID_EMAIL',
606
+ MIN_VALUE: 'ERRORS.MIN_VALUE',
607
+ MAX_VALUE: 'ERRORS.MAX_VALUE',
608
+ PASSWORD_MISMATCH: 'ERRORS.PASSWORD_MISMATCH',
609
+ NUMBERS_ONLY: 'ERRORS.NUMBERS_ONLY',
610
+ LETTERS_ONLY: 'ERRORS.LETTERS_ONLY',
611
+ ARABIC_ONLY: 'ERRORS.ARABIC_ONLY',
612
+ ENGLISH_ONLY: 'ERRORS.ENGLISH_ONLY',
613
+ ALPHANUMERIC_ONLY: 'ERRORS.ALPHANUMERIC_ONLY',
614
+ INVALID_PHONE: 'ERRORS.INVALID_PHONE',
615
+ /** Saudi mobile: must start with 05 and be 10 digits. */
616
+ INVALID_PHONE_SA_05: 'ERRORS.INVALID_PHONE_SA_05',
617
+ INVALID_URL: 'ERRORS.INVALID_URL',
618
+ INVALID_IBAN: 'ERRORS.INVALID_IBAN',
619
+ INVALID_CR_NUMBER: 'ERRORS.INVALID_CR_NUMBER',
620
+ WEAK_PASSWORD: 'ERRORS.WEAK_PASSWORD',
621
+ WHITESPACE_ONLY: 'ERRORS.WHITESPACE_ONLY',
622
+ DATE_BEFORE_MIN: 'ERRORS.DATE_BEFORE_MIN',
623
+ DATE_AFTER_MAX: 'ERRORS.DATE_AFTER_MAX',
624
+ CONFIRMATION_MISMATCH: 'ERRORS.CONFIRMATION_MISMATCH',
625
+ AT_LEAST_ONE_OPTION_REQUIRED: 'ERRORS.AT_LEAST_ONE_OPTION_REQUIRED',
626
+ PERCENTAGE_EXCEEDS_TOTAL: 'ERRORS.PERCENTAGE_EXCEEDS_TOTAL',
627
+ STAGE_TAKES_100: 'ERRORS.STAGE_TAKES_100',
628
+ TOTAL_LESS_THAN_100: 'ERRORS.TOTAL_LESS_THAN_100',
629
+ INVALID_SHORT_ADDRESS: 'ERRORS.INVALID_SHORT_ADDRESS',
630
+ };
631
+
632
+ /** i18n paths and default language for @ngx-translate. */
633
+ const I18N_CONFIG = {
634
+ DEFAULT_LANGUAGE: Language.AR,
635
+ TRANSLATION_PREFIX: './assets/i18n/',
636
+ TRANSLATION_SUFFIX: '.json',
637
+ };
638
+
639
+ const AR_TRANSLATION = {
640
+ accept: 'موافق',
641
+ addRule: 'إضافة قاعدة',
642
+ am: 'صباحًا',
643
+ apply: 'تطبيق',
644
+ cancel: 'إلغاء',
645
+ choose: 'اختر',
646
+ chooseDate: 'اختر اليوم',
647
+ chooseMonth: 'اختر الشهر',
648
+ chooseYear: 'اختر السنة',
649
+ clear: 'إزالة',
650
+ completed: 'مكتمل',
651
+ contains: 'يحتوي على',
652
+ dateAfter: 'التاريخ بعد',
653
+ dateBefore: 'التاريخ قبل',
654
+ dateFormat: 'yy/mm/dd',
655
+ dateIs: 'التاريخ هو',
656
+ dateIsNot: 'التاريخ ليس',
657
+ dayNames: ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
658
+ dayNamesMin: ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'],
659
+ dayNamesShort: ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'],
660
+ emptyFilterMessage: 'لا توجد اختيارات',
661
+ emptyMessage: 'لا توجد نتيجة',
662
+ emptySearchMessage: 'لا تتوفر بيانات',
663
+ emptySelectionMessage: 'لم يتم اختيار أي عنصر',
664
+ endsWith: 'ينتهي بـ',
665
+ equals: 'يساوي',
666
+ fileChosenMessage: '{0} ملفات',
667
+ fileSizeTypes: [
668
+ 'بايت',
669
+ 'كيلو بايت',
670
+ 'ميغابايت',
671
+ 'غيغابايت',
672
+ 'تيرابايت',
673
+ 'بيتابايت',
674
+ 'إكسابايت',
675
+ 'زيتابايت',
676
+ 'يوتابايت',
677
+ ],
678
+ firstDayOfWeek: 6,
679
+ gt: 'أكبر من',
680
+ gte: 'أكبر من أو يساوي',
681
+ lt: 'أقل من',
682
+ lte: 'أقل من أو يساوي',
683
+ matchAll: 'يطابق الكل',
684
+ matchAny: 'يطابق أي',
685
+ medium: 'متوسط',
686
+ monthNames: [
687
+ 'يناير',
688
+ 'فبراير',
689
+ 'مارس',
690
+ 'أبريل',
691
+ 'مايو',
692
+ 'يونيو',
693
+ 'يوليو',
694
+ 'أغسطس',
695
+ 'سبتمبر',
696
+ 'أكتوبر',
697
+ 'نوفمبر',
698
+ 'ديسمبر',
699
+ ],
700
+ monthNamesShort: [
701
+ 'يناير',
702
+ 'فبراير',
703
+ 'مارس',
704
+ 'أبريل',
705
+ 'مايو',
706
+ 'يونيو',
707
+ 'يوليو',
708
+ 'أغسطس',
709
+ 'سبتمبر',
710
+ 'أكتوبر',
711
+ 'نوفمبر',
712
+ 'ديسمبر',
713
+ ],
714
+ nextDecade: 'العقد القادم',
715
+ nextHour: 'الساعة التالية',
716
+ nextMinute: 'الدقيقة التالية',
717
+ nextMonth: 'الشهر التالي',
718
+ nextSecond: 'الثانية التالية',
719
+ nextYear: 'السنة التالية',
720
+ noFileChosenMessage: 'لم يتم اختيار أي ملف',
721
+ noFilter: 'بدون تصفية',
722
+ notContains: 'لا يحتوي على',
723
+ notEquals: 'لا يساوي',
724
+ passwordPrompt: 'أدخل كلمة المرور',
725
+ pending: 'قيد الانتظار',
726
+ pm: 'مساءً',
727
+ prevDecade: 'العقد السابق',
728
+ prevHour: 'الساعة السابقة',
729
+ prevMinute: 'الدقيقة السابقة',
730
+ prevMonth: 'الشهر السابق',
731
+ prevSecond: 'الثانية السابقة',
732
+ prevYear: 'السنة السابقة',
733
+ reject: 'لا',
734
+ removeRule: 'حذف قاعدة',
735
+ searchMessage: '{0} النتائج المتاحة',
736
+ selectionMessage: '{0} عناصر تم اختيارها',
737
+ startsWith: 'يبدأ بـ',
738
+ strong: 'قوي',
739
+ today: 'اليوم',
740
+ upload: 'تحميل',
741
+ weak: 'ضعيف',
742
+ weekHeader: 'الأسبوع',
743
+ };
744
+ const EN_TRANSLATION = {
745
+ accept: 'Yes',
746
+ addRule: 'Add Rule',
747
+ am: 'AM',
748
+ apply: 'Apply',
749
+ cancel: 'Cancel',
750
+ choose: 'Choose',
751
+ chooseDate: 'Choose Date',
752
+ chooseMonth: 'Choose Month',
753
+ chooseYear: 'Choose Year',
754
+ clear: 'Clear',
755
+ completed: 'Completed',
756
+ contains: 'Contains',
757
+ dateAfter: 'Date is after',
758
+ dateBefore: 'Date is before',
759
+ dateFormat: 'mm/dd/yy',
760
+ dateIs: 'Date is',
761
+ dateIsNot: 'Date is not',
762
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
763
+ dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
764
+ dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
765
+ emptyFilterMessage: 'No results found',
766
+ emptyMessage: 'No available options',
767
+ emptySearchMessage: 'No results found',
768
+ emptySelectionMessage: 'No selected item',
769
+ endsWith: 'Ends with',
770
+ equals: 'Equals',
771
+ fileChosenMessage: '{0} files',
772
+ fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
773
+ firstDayOfWeek: 0,
774
+ gt: 'Greater than',
775
+ gte: 'Greater than or equal to',
776
+ lt: 'Less than',
777
+ lte: 'Less than or equal to',
778
+ matchAll: 'Match All',
779
+ matchAny: 'Match Any',
780
+ medium: 'Medium',
781
+ monthNames: [
782
+ 'January',
783
+ 'February',
784
+ 'March',
785
+ 'April',
786
+ 'May',
787
+ 'June',
788
+ 'July',
789
+ 'August',
790
+ 'September',
791
+ 'October',
792
+ 'November',
793
+ 'December',
794
+ ],
795
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
796
+ nextDecade: 'Next Decade',
797
+ nextHour: 'Next Hour',
798
+ nextMinute: 'Next Minute',
799
+ nextMonth: 'Next Month',
800
+ nextSecond: 'Next Second',
801
+ nextYear: 'Next Year',
802
+ noFileChosenMessage: 'No file chosen',
803
+ noFilter: 'No Filter',
804
+ notContains: 'Not contains',
805
+ notEquals: 'Not equals',
806
+ passwordPrompt: 'Enter a password',
807
+ pending: 'Pending',
808
+ pm: 'PM',
809
+ prevDecade: 'Previous Decade',
810
+ prevHour: 'Previous Hour',
811
+ prevMinute: 'Previous Minute',
812
+ prevMonth: 'Previous Month',
813
+ prevSecond: 'Previous Second',
814
+ prevYear: 'Previous Year',
815
+ reject: 'No',
816
+ removeRule: 'Remove Rule',
817
+ searchMessage: '{0} results are available',
818
+ selectionMessage: '{0} items selected',
819
+ startsWith: 'Starts with',
820
+ strong: 'Strong',
821
+ today: 'Today',
822
+ upload: 'Upload',
823
+ weak: 'Weak',
824
+ weekHeader: 'Wk',
825
+ };
826
+ const PRIMENG_LOCALE = {
827
+ [Language.AR]: AR_TRANSLATION,
828
+ [Language.EN]: EN_TRANSLATION,
829
+ };
830
+
831
+ const ASSET_PATHS = {
832
+ NCIM_LOGO: 'assets/images/ncim-logo.svg',
833
+ NAFATH_ICON: 'assets/images/nafath-icon.svg',
834
+ REGISTER_SIDEBAR: 'assets/images/register.png',
835
+ SIDEBAR_BG: 'assets/images/bg-side.png',
836
+ SUCCESS: 'assets/images/Success.png',
837
+ SAUDI_FLAG: 'assets/images/saudi-flag.svg',
838
+ CAPTCHA_IMAGE: 'assets/images/ver.png',
839
+ };
840
+
841
+ const ICONS = {
842
+ PAGES_ICONS: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M19.002 1.25977C20.5198 1.26086 21.7498 2.49192 21.75 4.00977V19.999C21.7497 21.5176 20.5186 22.749 19 22.749H5C3.48137 22.749 2.25025 21.5176 2.25 19.999V4C2.25 2.48045 3.48241 1.24892 5.00195 1.25L19.002 1.25977ZM5.00098 2.75C4.31027 2.7495 3.75 3.30929 3.75 4V19.999C3.75025 20.6892 4.3098 21.249 5 21.249H19C19.6902 21.249 20.2497 20.6892 20.25 19.999V4.00977C20.2498 3.31995 19.6908 2.7603 19.001 2.75977L5.00098 2.75ZM10.5557 13.75C10.9698 13.7501 11.3057 14.0858 11.3057 14.5C11.3057 14.9142 10.9698 15.2499 10.5557 15.25H7C6.58579 15.25 6.25 14.9142 6.25 14.5C6.25 14.0858 6.58579 13.75 7 13.75H10.5557ZM15 8.75C15.4142 8.75 15.75 9.08579 15.75 9.5C15.75 9.91421 15.4142 10.25 15 10.25H7C6.58579 10.25 6.25 9.91421 6.25 9.5C6.25 9.08579 6.58579 8.75 7 8.75H15Z" fill="currentColor"/></svg>',
843
+ PRINT_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M14.25 0C15.4926 0 16.5 1.00736 16.5 2.25V6H17.75C19.8211 6 21.5 7.67893 21.5 9.75V15.75C21.5 16.7165 20.7165 17.5 19.75 17.5H16.5V18.75C16.5 20.2688 15.2688 21.5 13.75 21.5H7.75C6.23122 21.5 5 20.2688 5 18.75V17.5H1.75C0.783502 17.5 0 16.7165 0 15.75V9.75C0 7.67893 1.67893 6 3.75 6H5V2.25C5 1.00736 6.00736 0 7.25 0H14.25ZM6.5 18.75C6.5 19.4404 7.05964 20 7.75 20H13.75C14.4404 20 15 19.4404 15 18.75V15.5H6.5V18.75ZM3.75 7.5C2.50736 7.5 1.5 8.50736 1.5 9.75V15.75C1.5 15.8881 1.61193 16 1.75 16H5V14.75C5 14.3358 5.33579 14 5.75 14H15.75C16.1642 14 16.5 14.3358 16.5 14.75V16H19.75C19.8881 16 20 15.8881 20 15.75V9.75C20 8.50736 18.9926 7.5 17.75 7.5H3.75ZM17.2588 9.25C17.8111 9.25 18.2588 9.69771 18.2588 10.25C18.2588 10.8023 17.8111 11.25 17.2588 11.25H17.25C16.6977 11.25 16.25 10.8023 16.25 10.25C16.25 9.69771 16.6977 9.25 17.25 9.25H17.2588ZM7.25 1.5C6.83579 1.5 6.5 1.83579 6.5 2.25V6H15V2.25C15 1.83579 14.6642 1.5 14.25 1.5H7.25Z" fill="currentColor"/></svg>',
844
+ DELETE_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="22" viewBox="0 0 20 22" fill="none"><path d="M11.5908 0C12.2908 8.59975e-05 12.9235 0.41719 13.1992 1.06055L14.2441 3.5H18.75C19.1642 3.5 19.5 3.83579 19.5 4.25C19.5 4.66421 19.1642 5 18.75 5H17.9561L17.1123 18.916C17.0243 20.3675 15.8214 21.5 14.3672 21.5H5.13281C3.67864 21.5 2.47567 20.3675 2.3877 18.916L1.54395 5H0.75C0.335786 5 0 4.66421 0 4.25C0 3.83579 0.335786 3.5 0.75 3.5H5.25586L6.30078 1.06055C6.57652 0.41719 7.20923 8.59974e-05 7.90918 0H11.5908ZM3.88477 18.8252C3.92475 19.485 4.47182 20 5.13281 20H14.3672C15.0282 20 15.5752 19.485 15.6152 18.8252L16.4531 5H3.04688L3.88477 18.8252ZM11.25 13.5C11.6642 13.5 12 13.8358 12 14.25C12 14.6642 11.6642 15 11.25 15H8.25C7.83579 15 7.5 14.6642 7.5 14.25C7.5 13.8358 7.83579 13.5 8.25 13.5H11.25ZM12.75 9.5C13.1642 9.5 13.5 9.83579 13.5 10.25C13.5 10.6642 13.1642 11 12.75 11H6.75C6.33579 11 6 10.6642 6 10.25C6 9.83579 6.33579 9.5 6.75 9.5H12.75ZM7.90918 1.5C7.80932 1.50009 7.71909 1.5596 7.67969 1.65137L6.8877 3.5H12.6123L11.8203 1.65137C11.7809 1.5596 11.6907 1.50009 11.5908 1.5H7.90918Z" fill="currentColor"/></svg>',
845
+ COPY_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><g clip-path="url(#clip0_5613_101839)"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.335 0.841797C14.3468 0.842691 15.1669 1.66294 15.167 2.67481V9.33496C15.167 10.3475 14.3455 11.1689 13.333 11.1689H12.5V13.334C12.5 14.3464 11.6794 15.1678 10.667 15.168H2.66699C1.65447 15.168 0.833008 14.3465 0.833008 13.334V6.00293C0.833008 4.9895 1.65556 4.16866 2.66895 4.16992L3.5 4.1709V2.66992C3.5 1.65692 4.32196 0.835265 5.33496 0.835938L13.335 0.841797ZM2.66797 5.16992C2.20727 5.16927 1.83301 5.54223 1.83301 6.00293V13.334C1.83301 13.7942 2.20675 14.168 2.66699 14.168H10.667C11.1271 14.1678 11.5 13.7941 11.5 13.334V11.1689H5.33301C4.32064 11.1688 3.5 10.3474 3.5 9.33496V5.1709L2.66797 5.16992ZM5.33398 1.83594C4.87353 1.83563 4.5 2.20947 4.5 2.66992V9.33496C4.5 9.79509 4.87292 10.1688 5.33301 10.1689H13.333C13.7932 10.1689 14.167 9.7952 14.167 9.33496V2.67481C14.1669 2.21487 13.7939 1.84216 13.334 1.8418L5.33398 1.83594Z" fill="#384250"/></g><defs><clipPath id="clip0_5613_101839"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
846
+ MESSAGE_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="96" height="94" fill="none" viewBox="0 0 96 94"><g filter="url(#a)"><path fill="url(#b)" d="M23.47 44.623c2.974 2.337 6.679 3.59 10.415 4.204q.49.085.983.147 1.134.158 2.275.255a3.85 3.85 0 0 1 3.519 3.836v5.255c0 .794.984 1.184 1.51.588 1.878-2.134 4.901-3.994 8.069-5.517.925-.451 1.867-.868 2.801-1.253 5.212-2.18 10.038-3.409 10.038-3.409q1.206-.28 2.377-.673c.048-.016.09-.024.13-.04q.331-.111.647-.22l.09-.034v-.008c3.287-1.205 6.31-3.073 8.547-5.736 3.147-3.753 4.474-8.71 4.974-13.577.64-6.294-.05-13.103-3.531-18.527C72.78 4.4 66.882 1.416 60.524.474c-5.425-.811-10.939-.36-16.43-.246C37.4.376 30.207.146 24.61 3.833c-3.195 2.106-5.548 5.352-6.973 8.908-1.434 3.556-1.983 7.408-2.065 11.242-.171 7.637 1.877 15.92 7.9 20.64"/><path fill="url(#c)" d="M62.292 37.959c-1.77 1.389-3.974 2.135-6.196 2.501a13 13 0 0 1-.62.092c-1.94.27-3.413 1.872-3.413 3.83v.863c0 .754-.9 1.16-1.452.647-1.112-1.035-2.65-1.948-4.248-2.717-.55-.268-1.11-.517-1.667-.745-3.1-1.297-5.972-2.029-5.972-2.029q-.72-.166-1.415-.4c-.029-.01-.053-.015-.077-.024l-.44-.152v-.004c-1.954-.717-3.754-1.829-5.084-3.414-1.873-2.232-2.662-5.181-2.96-8.078-.38-3.744.03-7.797 2.1-11.024 2.102-3.281 5.613-5.056 9.397-5.616 3.227-.483 6.509-.215 9.775-.147 3.983.088 8.264-.049 11.594 2.145 1.901 1.253 3.301 3.185 4.15 5.3.853 2.117 1.18 4.407 1.228 6.69.102 4.544-1.116 9.473-4.7 12.282" opacity=".51" style="mix-blend-mode:multiply"/><path fill="url(#d)" d="m44.799 49.56 5.44 3.828a50 50 0 0 1 2.802-1.253c5.212-2.18 10.038-3.408 10.038-3.408s-5.162 1.35-18.28.833" opacity=".51" style="mix-blend-mode:multiply"/><path fill="#fff" d="M62.292 35.817c-1.77 1.39-3.974 2.136-6.196 2.501q-.294.052-.586.088l-.034.005c-1.94.27-3.413 1.872-3.413 3.83v.863c0 .754-.9 1.16-1.452.647-1.112-1.035-2.65-1.948-4.248-2.716-.55-.269-1.11-.517-1.667-.747-3.1-1.297-5.972-2.028-5.972-2.028q-.72-.167-1.415-.4c-.029-.01-.053-.015-.077-.024l-.44-.151v-.005c-1.954-.716-3.754-1.828-5.084-3.413-1.873-2.233-2.662-5.183-2.96-8.079-.38-3.745.03-7.796 2.1-11.023 2.102-3.282 5.613-5.056 9.397-5.617 3.227-.483 6.509-.215 9.775-.147 3.983.088 8.264-.048 11.594 2.146 1.901 1.253 3.301 3.183 4.15 5.3.853 2.116 1.18 4.407 1.228 6.688.102 4.544-1.116 9.474-4.7 12.282"/><path fill="url(#e)" d="m49.6 38.754-3.237 2.278c-.55-.268-1.11-.517-1.667-.747-3.101-1.297-5.972-2.027-5.972-2.027s3.07.803 10.877.496" opacity=".51" style="mix-blend-mode:multiply"/><path fill="url(#f)" d="M54.255 24.653a3.707 3.707 0 1 0 7.414 0 3.707 3.707 0 0 0-7.414 0" opacity=".51" style="mix-blend-mode:multiply"/><path fill="url(#g)" d="M44.103 24.653a3.707 3.707 0 1 0 7.414 0 3.707 3.707 0 0 0-7.414 0" opacity=".51" style="mix-blend-mode:multiply"/><path fill="url(#h)" d="M33.95 24.653a3.707 3.707 0 1 0 7.415 0 3.707 3.707 0 0 0-7.415 0" opacity=".51" style="mix-blend-mode:multiply"/></g><defs><linearGradient id="b" x1="108.145" x2="75.64" y1="95.649" y2="1.916" gradientUnits="userSpaceOnUse"><stop stop-color="#19004d"/><stop offset="1" stop-color="#54c08a"/></linearGradient><linearGradient id="c" x1="31.993" x2="31.312" y1="17.063" y2="50.677" gradientUnits="userSpaceOnUse"><stop offset=".606" stop-color="#19004d" stop-opacity=".91"/><stop offset=".746" stop-color="#a58cf4" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="54.82" x2="50.338" y1="44.667" y2="56.993" gradientUnits="userSpaceOnUse"><stop offset=".512" stop-color="#19004d" stop-opacity=".91"/><stop offset=".76" stop-color="#a58cf4" stop-opacity="0"/></linearGradient><linearGradient id="e" x1="43.638" x2="46.304" y1="35.842" y2="43.176" gradientUnits="userSpaceOnUse"><stop offset=".512" stop-color="#1b8354"/><stop offset=".76" stop-color="#1b8354" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="66.672" x2="56.235" y1="32.49" y2="8.404" gradientUnits="userSpaceOnUse"><stop offset=".267" stop-color="#1b8354"/><stop offset=".63" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="g" x1="56.52" x2="46.083" y1="32.49" y2="8.404" gradientUnits="userSpaceOnUse"><stop offset=".267" stop-color="#1b8354"/><stop offset=".63" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="46.368" x2="35.931" y1="32.49" y2="8.404" gradientUnits="userSpaceOnUse"><stop offset=".267" stop-color="#1b8354"/><stop offset=".63" stop-color="#fff" stop-opacity="0"/></linearGradient><filter id="a" width="95.62" height="93.256" x="0" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feMorphology in="SourceAlpha" radius="14.439" result="effect1_dropShadow_6167_40545"/><feOffset dy="18.5"/><feGaussianBlur stdDeviation="15"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.105882 0 0 0 0 0.513726 0 0 0 0 0.329412 0 0 0 0.24 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_6167_40545"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_6167_40545" result="shape"/></filter></defs></svg>',
847
+ SAR_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" fill="none" viewBox="0 0 16 17"><path fill="#161616" d="M15.213 13.022a6.7 6.7 0 0 0 .558-2.084l-4.807 1.022V9.995l4.249-.903a6.7 6.7 0 0 0 .558-2.084l-4.807 1.021V.961A6.8 6.8 0 0 0 9.04 2.575v5.863l-1.922.409V0a6.8 6.8 0 0 0-1.923 1.613v7.642l-4.301.914a6.7 6.7 0 0 0-.559 2.084l4.86-1.032v2.475l-5.208 1.106a6.7 6.7 0 0 0-.558 2.085l5.452-1.159a1.73 1.73 0 0 0 1.073-.716l1-1.482a.96.96 0 0 0 .164-.538v-2.18l1.922-.409v3.931z"/></svg>',
848
+ LAYERS_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="18" fill="none" viewBox="0 0 17 18"><path fill="#1f2a37" d="M14.296 11.785c.28-.36.79-.42 1.142-.133.435.356.812.846.812 1.529 0 .862-.593 1.408-1.173 1.79-.583.385-1.426.779-2.44 1.253l-1.447.675-.156.074c-1.19.557-2.016.943-2.909.943s-1.718-.386-2.91-.943l-.154-.074-1.448-.675c-1.014-.474-1.857-.868-2.44-1.252C.593 14.589 0 14.042 0 13.18c0-.683.377-1.173.813-1.529a.8.8 0 0 1 1.141.133c.28.36.222.884-.129 1.171a.8.8 0 0 0-.19.197c-.01.019-.01.024-.01.027l.001.008a.2.2 0 0 0 .03.05c.053.07.165.18.396.331.472.311 1.206.657 2.298 1.167l1.384.647c1.41.659 1.895.866 2.391.866s.981-.207 2.39-.866l1.385-.647c1.092-.51 1.826-.856 2.298-1.167.23-.152.343-.261.396-.33a.2.2 0 0 0 .03-.05l.001-.008v-.001c0-.003 0-.008-.01-.027a.8.8 0 0 0-.19-.197.85.85 0 0 1-.13-1.17m0-4.27c.28-.36.79-.42 1.142-.133.435.356.812.846.812 1.528 0 .862-.593 1.408-1.173 1.79-.583.384-1.426.78-2.44 1.253l-1.447.676-.156.073c-1.19.557-2.016.944-2.909.944s-1.718-.387-2.91-.944l-.154-.073-1.448-.676c-1.014-.474-1.857-.869-2.44-1.253C.593 10.318 0 9.772 0 8.91c0-.682.377-1.172.813-1.528a.8.8 0 0 1 1.141.133c.28.36.222.884-.129 1.17a.8.8 0 0 0-.19.198c-.01.018-.01.022-.01.025v.002l.001.008c.001.003.008.02.03.05.053.07.165.18.396.33.472.312 1.206.657 2.298 1.167l1.384.647c1.41.659 1.895.867 2.391.867s.981-.208 2.39-.867l1.385-.647c1.092-.51 1.826-.855 2.298-1.166.23-.152.343-.262.396-.331a.2.2 0 0 0 .03-.05l.001-.008v-.002c0-.003 0-.007-.01-.025a.8.8 0 0 0-.19-.197.85.85 0 0 1-.13-1.171M8.125 0c.871 0 1.68.374 2.928.951l.132.06 1.47.68c1.052.486 1.905.882 2.49 1.263.586.382 1.105.879 1.105 1.629s-.519 1.247-1.104 1.629-1.44.777-2.49 1.263l-1.471.68-.132.06c-1.249.577-2.057.951-2.928.951s-1.68-.374-2.928-.951l-.132-.06-1.47-.68c-1.052-.486-1.905-.882-2.49-1.263C.518 5.83 0 5.334 0 4.583c0-.75.519-1.247 1.104-1.629s1.44-.777 2.49-1.263l1.471-.68.132-.06C6.446.374 7.254 0 8.125 0"/></svg>',
849
+ FILE_ICON: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" fill="none" viewBox="0 0 18 17"><path fill="#1b8354" d="M6.904 0a1.63 1.63 0 0 1 1.348.705l1.14 1.67h4.428c1.342 0 2.43 1.063 2.43 2.375v1.917h.184c.97 0 1.715.922 1.419 1.882l-2.059 6.667a1.48 1.48 0 0 1-1.418 1.034H2.855c-.694 0-1.311-.477-1.454-1.169L.031 8.414c-.19-.925.537-1.747 1.452-1.747h.183V1.583C1.666.709 2.392 0 3.286 0zM3.286 6.667H14.63V4.75a.8.8 0 0 0-.81-.792H8.96a.82.82 0 0 1-.676-.353L6.904 1.583H3.286z"/></svg>',
850
+ INFO_CIRCLE: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
851
+ SAND_GLASS: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/></svg>',
852
+ };
853
+
854
+ /**
855
+ * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY.
856
+ * Run "npm run icons:sync" to regenerate.
857
+ */
858
+ const GENERATED_ICON_NAMES = [
859
+ 'active-group',
860
+ 'activity',
861
+ 'activity-background',
862
+ 'activity-regular',
863
+ 'add-user',
864
+ 'ai-magic',
865
+ 'aiIcon',
866
+ 'alarm',
867
+ 'alarm-clock-background',
868
+ 'album-02',
869
+ 'alert',
870
+ 'alert-01',
871
+ 'alert-background',
872
+ 'analytics-down',
873
+ 'analytics-down-background',
874
+ 'analyticsBackground',
875
+ 'arrow-left',
876
+ 'arrow-left-01',
877
+ 'arrow-up-left',
878
+ 'arrowUp',
879
+ 'arrowUpWarning',
880
+ 'Avatar-details',
881
+ 'Ball',
882
+ 'bell',
883
+ 'block',
884
+ 'book',
885
+ 'bookmark',
886
+ 'brain',
887
+ 'briefcase',
888
+ 'briefcase-06',
889
+ 'bubble-chat-add',
890
+ 'building',
891
+ 'building-section',
892
+ 'building-settings',
893
+ 'buildingBackground',
894
+ 'cakender-plus',
895
+ 'calendar',
896
+ 'calendar-04',
897
+ 'calendar-icon',
898
+ 'calendar-minus',
899
+ 'calendar-plus',
900
+ 'camera',
901
+ 'change-screen-mode',
902
+ 'chart-bar',
903
+ 'chart-line',
904
+ 'chat',
905
+ 'chatPlus',
906
+ 'check',
907
+ 'check-circle',
908
+ 'checked',
909
+ 'chevron-down',
910
+ 'chevronDown',
911
+ 'chevronLeft',
912
+ 'chevronRight',
913
+ 'circle-check',
914
+ 'clipboard',
915
+ 'clock',
916
+ 'close',
917
+ 'close-popup',
918
+ 'close-side',
919
+ 'cog',
920
+ 'colored-file',
921
+ 'columns',
922
+ 'commercial-section',
923
+ 'compare',
924
+ 'confirm-add',
925
+ 'copy',
926
+ 'credit-card',
927
+ 'customer-service',
928
+ 'dashboard',
929
+ 'datepicker',
930
+ 'datepicker-arrow',
931
+ 'datepicker-chevron-down',
932
+ 'decreaseArrow',
933
+ 'delete',
934
+ 'dislike',
935
+ 'distribute-horizontal-center',
936
+ 'docs-validation',
937
+ 'document',
938
+ 'download',
939
+ 'download-small',
940
+ 'download2',
941
+ 'edit',
942
+ 'edit-02',
943
+ 'ellipsis',
944
+ 'empty-companies',
945
+ 'empty-module',
946
+ 'entertainment-section',
947
+ 'entity-building',
948
+ 'eye',
949
+ 'eyeOff',
950
+ 'feedback',
951
+ 'Feedback-Icon',
952
+ 'fees',
953
+ 'ferrisWheelBackground',
954
+ 'fileUpload',
955
+ 'fill-worring',
956
+ 'filter',
957
+ 'finalSeccess',
958
+ 'fire',
959
+ 'folder',
960
+ 'full-page',
961
+ 'gavel',
962
+ 'grean-file',
963
+ 'hand',
964
+ 'hand-raised',
965
+ 'help',
966
+ 'help-circle',
967
+ 'hold',
968
+ 'hold-background',
969
+ 'home',
970
+ 'home-01',
971
+ 'honour-star',
972
+ 'hourglass',
973
+ 'incognito',
974
+ 'increaseArrow',
975
+ 'info',
976
+ 'information-circle',
977
+ 'layers',
978
+ 'layers-01',
979
+ 'layers-2',
980
+ 'layers-background',
981
+ 'levels',
982
+ 'like',
983
+ 'link',
984
+ 'list',
985
+ 'loading',
986
+ 'logo',
987
+ 'logout',
988
+ 'logout-popup',
989
+ 'mail',
990
+ 'map',
991
+ 'massage',
992
+ 'menu',
993
+ 'modelIcon',
994
+ 'muslim',
995
+ 'nafath-icon',
996
+ 'new-office',
997
+ 'new-office-background',
998
+ 'not-active-group',
999
+ 'note-stack',
1000
+ 'notifcation',
1001
+ 'objection',
1002
+ 'open-book',
1003
+ 'opened-folder',
1004
+ 'outOfWallet',
1005
+ 'package',
1006
+ 'pdf',
1007
+ 'pencil',
1008
+ 'period',
1009
+ 'person',
1010
+ 'phone',
1011
+ 'play',
1012
+ 'plus',
1013
+ 'preference-vertical',
1014
+ 'print',
1015
+ 'pulse',
1016
+ 'punishment',
1017
+ 'Rank',
1018
+ 'record',
1019
+ 'red-alert',
1020
+ 'refresh',
1021
+ 'regularArrowLeft',
1022
+ 'remove',
1023
+ 'sandClock',
1024
+ 'sar',
1025
+ 'sar-icon',
1026
+ 'saudi-flag',
1027
+ 'scale',
1028
+ 'search',
1029
+ 'search-black',
1030
+ 'search-list',
1031
+ 'search2',
1032
+ 'settings',
1033
+ 'settings-background',
1034
+ 'settings2',
1035
+ 'shape',
1036
+ 'shape-square',
1037
+ 'shield',
1038
+ 'sort',
1039
+ 'sort-direction',
1040
+ 'sort-down',
1041
+ 'sort-up',
1042
+ 'sort2',
1043
+ 'stack',
1044
+ 'start-chat',
1045
+ 'status',
1046
+ 'store',
1047
+ 'success-company',
1048
+ 'support',
1049
+ 'TagSimple',
1050
+ 'time-quarter-02',
1051
+ 'total-group',
1052
+ 'total-requests',
1053
+ 'total-workingdays',
1054
+ 'traingles',
1055
+ 'trash',
1056
+ 'trash-bin',
1057
+ 'trash-icon-fill',
1058
+ 'trend-down-arrow',
1059
+ 'trend-up-arrow',
1060
+ 'up-arrow-green',
1061
+ 'Up-arrrow',
1062
+ 'user',
1063
+ 'user-placeholder',
1064
+ 'user-plus',
1065
+ 'wallet',
1066
+ 'wallet-02',
1067
+ 'wallet-background',
1068
+ 'wallet2',
1069
+ 'warning',
1070
+ 'widget',
1071
+ 'workflow',
1072
+ ];
1073
+
1074
+ const PHONE_CONFIG = {
1075
+ COUNTRY_CODE: '+966',
1076
+ FLAG_ICON: 'saudi-flag',
1077
+ };
1078
+ /** i18n keys for form placeholders. Values live in ar.json / en.json only. */
1079
+ const PLACEHOLDER_KEYS = {
1080
+ EMAIL: 'AUTH.REGISTER.EMAIL_PLACEHOLDER',
1081
+ MOBILE: 'AUTH.REGISTER.MOBILE_PLACEHOLDER',
1082
+ };
1083
+
1084
+ const STORAGE_KEYS = {
1085
+ AUTH_TOKEN: 'ncim_auth_token',
1086
+ REFRESH_TOKEN: 'ncim_refresh_token',
1087
+ USER_PROFILE: 'ncim_user_profile',
1088
+ LANGUAGE: 'ncim_language',
1089
+ REGISTRATION_DRAFT: 'ncim_registration_draft',
1090
+ PERMISSIONS: 'ncim_permissions',
1091
+ NAFATH_IDENTITY: 'ncim_nafath_identity',
1092
+ };
1093
+
1094
+ const CHART_COLORS = {
1095
+ green: '#66BB9B',
1096
+ greenLight: '#A8D5BA',
1097
+ greenLeaf: '#C8E6C9',
1098
+ greenTeal: '#B8E0D0',
1099
+ red: '#E57373',
1100
+ redLight: '#F4A0A0',
1101
+ redSoft: '#FFCDD2',
1102
+ orange: '#F5A623',
1103
+ yellow: '#F5E6B8',
1104
+ purple: '#C4B5E0',
1105
+ blue: '#B3E5FC',
1106
+ trendUp: '#10B981',
1107
+ trendDown: '#EF4444',
1108
+ /** Matches `--color-report-rate-green` (license / KPI sparklines). */
1109
+ reportRateGreen: '#88D8AD',
1110
+ };
1111
+ /** Saudi Arabia center (Riyadh) and zoom to fit country on open */
1112
+ const MAP_CONFIG = {
1113
+ tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
1114
+ maxZoom: 19,
1115
+ defaultCenter: { lat: 24.7136, lng: 46.6753 }, // Riyadh, Saudi Arabia
1116
+ defaultZoom: 6, // Zoom level to show Saudi Arabia on open
1117
+ markerSize: [280, 120],
1118
+ markerAnchor: [140, 130],
1119
+ scoreThreshold: 50,
1120
+ };
1121
+
1122
+ /** Map legend and card badge config. Used by legend popup and map cards. */
1123
+ const MAP_LEGEND_CONFIG = {
1124
+ [MapLegendStatus.UNCORRECTED_VIOLATION]: {
1125
+ icon: 'alert-01',
1126
+ bg: '#FEF3F2',
1127
+ color: '#EF4444',
1128
+ labelKey: 'REGULATORY_DASHBOARD.MAP_LEGEND_UNCORRECTED_VIOLATION',
1129
+ },
1130
+ [MapLegendStatus.UPCOMING_VISIT]: {
1131
+ icon: 'briefcase-06',
1132
+ bg: '#F1EAF4',
1133
+ color: '#80519F',
1134
+ labelKey: 'REGULATORY_DASHBOARD.MAP_LEGEND_UPCOMING_VISIT',
1135
+ },
1136
+ [MapLegendStatus.CORRECTION_PERIOD]: {
1137
+ icon: 'loading',
1138
+ bg: '#EFF8FF',
1139
+ color: '#3B82F6',
1140
+ labelKey: 'REGULATORY_DASHBOARD.MAP_LEGEND_CORRECTION_PERIOD',
1141
+ },
1142
+ [MapLegendStatus.LICENSE_EXPIRING]: {
1143
+ icon: 'time-quarter-02',
1144
+ bg: '#FFFAEB',
1145
+ color: '#D97706',
1146
+ labelKey: 'REGULATORY_DASHBOARD.MAP_LEGEND_LICENSE_EXPIRING',
1147
+ },
1148
+ };
1149
+ /** Map card action icons (briefcase, time) with background and color. */
1150
+ const MAP_ACTION_ICONS = {
1151
+ briefcase: {
1152
+ icon: 'briefcase-06',
1153
+ bg: '#F1EAF4',
1154
+ color: '#80519F',
1155
+ },
1156
+ time: {
1157
+ icon: 'time-quarter-02',
1158
+ bg: '#FFFAEB',
1159
+ color: '#D97706',
1160
+ },
1161
+ };
1162
+
1163
+ /** Delay (ms) before showing skeleton after loading ends — used by ncim-grid-table, ncim-cards-statistics, etc. */
1164
+ const LOADING_DELAY_MS = 300;
1165
+
1166
+ /** API paths that never carry a bearer token. */
1167
+ const PUBLIC_API_PATHS = [
1168
+ '/auth/login',
1169
+ '/auth/register',
1170
+ '/auth/forgot-password',
1171
+ '/auth/verify-otp',
1172
+ '/auth/reset-password',
1173
+ '/auth/refresh-token',
1174
+ // MOMRAH SSO endpoints — token exchange and JWKS are unauthenticated requests
1175
+ 'ssoappdev.momra.gov.sa',
1176
+ 'sso.momra.gov.sa',
1177
+ ];
1178
+ /** Custom HTTP header names injected by interceptors. */
1179
+ const HTTP_HEADERS = {
1180
+ CORRELATION_ID: 'X-Correlation-Id',
1181
+ ACCEPT_LANGUAGE: 'Accept-Language',
1182
+ AUTHORIZATION: 'Authorization',
1183
+ RETRY_AFTER: 'Retry-After',
1184
+ MAINTENANCE: 'X-Maintenance',
1185
+ };
1186
+ /** Toast auto-dismiss durations (ms). */
1187
+ const TOAST_DURATION = {
1188
+ DEFAULT: 5_000,
1189
+ ERROR: 8_000,
1190
+ };
1191
+ /** Deduplication window (ms) — identical toasts within this window are suppressed. */
1192
+ const TOAST_DEDUP_MS = 2_000;
1193
+ /** Maximum dedup map entries before garbage-collecting stale keys. */
1194
+ const TOAST_DEDUP_MAX_SIZE = 50;
1195
+ /** Debounce period (ms) to prevent 401 flood from parallel requests. */
1196
+ const LOGOUT_DEBOUNCE_MS = 3_000;
1197
+ /** LRU HTTP response cache defaults. */
1198
+ const CACHE_DEFAULTS = {
1199
+ MAX_SIZE: 100,
1200
+ TTL_MS: 5 * 60 * 1_000,
1201
+ };
1202
+ /** Retry interceptor defaults. */
1203
+ const RETRY_DEFAULTS = {
1204
+ MAX_COUNT: 2,
1205
+ BACKOFF_BASE_MS: 1_000,
1206
+ };
1207
+ /** HTTP status codes eligible for automatic retry. */
1208
+ const RETRYABLE_STATUS_CODES = new Set([0, 502, 503, 504]);
1209
+ /** HTTP methods safe to retry (idempotent). */
1210
+ const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
1211
+ /** Max segments used when extracting a cache invalidation base-path from a mutation URL. */
1212
+ const CACHE_PATH_DEPTH = 3;
1213
+ /** Maximum characters to show for an API error message before truncating. */
1214
+ const ERROR_MESSAGE_MAX_LENGTH = 300;
1215
+ /** Maximum field-level errors displayed in a single toast. */
1216
+ const MAX_FIELD_ERRORS_SHOWN = 5;
1217
+ /** Fallback language when TranslateService has no current/default lang. */
1218
+ const FALLBACK_LANGUAGE = Language.AR;
1219
+ /** URL pattern for asset requests — errors for these are not shown as toasts. */
1220
+ const ASSET_URL_PATTERN = /\/assets\//;
1221
+
1222
+ /** Validates Saudi identity number format (starts with 1 or 2, 10 digits). */
1223
+ function identityNumberValidator() {
1224
+ return (control) => {
1225
+ if (!control.value) {
1226
+ return null;
1227
+ }
1228
+ return VALIDATION_PATTERNS.IDENTITY_NUMBER.test(control.value) ? null : { identityInvalid: true };
1229
+ };
1230
+ }
1231
+
1232
+ function idNumberValidator() {
1233
+ return (control) => {
1234
+ if (!control.value) {
1235
+ return null;
1236
+ }
1237
+ return VALIDATION_PATTERNS.ID_NUMBER.test(control.value) ? null : { idNumberInvalid: true };
1238
+ };
1239
+ }
1240
+
1241
+ function saudiMobileValidator() {
1242
+ return (control) => {
1243
+ if (!control.value) {
1244
+ return null;
1245
+ }
1246
+ return VALIDATION_PATTERNS.MOBILE_SAUDI.test(control.value) ? null : { invalidPhone: true };
1247
+ };
1248
+ }
1249
+
1250
+ /** Saudi mobile: `05` + 8 digits (10 characters total). */
1251
+ function saudiMobile05Validator() {
1252
+ return (control) => {
1253
+ if (!control.value) {
1254
+ return null;
1255
+ }
1256
+ const value = String(control.value).trim();
1257
+ return VALIDATION_PATTERNS.MOBILE_SAUDI_05.test(value) ? null : { invalidPhoneSa05: true };
1258
+ };
1259
+ }
1260
+
1261
+ /** Form-level validator: confirm field must match password field. Default keys: newPassword, confirmPassword. */
1262
+ function passwordMatchValidator(passwordKey = 'newPassword', confirmKey = 'confirmPassword') {
1263
+ return (control) => {
1264
+ const password = control.get(passwordKey);
1265
+ const confirmPassword = control.get(confirmKey);
1266
+ if (password && confirmPassword && password.value !== confirmPassword.value) {
1267
+ confirmPassword.setErrors({ ...confirmPassword.errors, passwordMismatch: true });
1268
+ return { passwordMismatch: true };
1269
+ }
1270
+ if (confirmPassword?.hasError('passwordMismatch')) {
1271
+ const { passwordMismatch, ...otherErrors } = confirmPassword.errors || {};
1272
+ const hasOtherErrors = Object.keys(otherErrors).length > 0;
1273
+ confirmPassword.setErrors(hasOtherErrors ? otherErrors : null);
1274
+ }
1275
+ return null;
1276
+ };
1277
+ }
1278
+
1279
+ /** Validates password has upper, lower, number, and special character. */
1280
+ function passwordStrengthValidator() {
1281
+ return (control) => {
1282
+ if (!control.value) {
1283
+ return null;
1284
+ }
1285
+ const value = control.value;
1286
+ const hasUpperCase = REGEX.PASSWORD_UPPERCASE.test(value);
1287
+ const hasLowerCase = REGEX.PASSWORD_LOWERCASE.test(value);
1288
+ const hasNumber = REGEX.PASSWORD_NUMBER.test(value);
1289
+ const hasSpecialChar = REGEX.PASSWORD_SPECIAL.test(value);
1290
+ const isStrong = hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
1291
+ return isStrong ? null : { weakPassword: true };
1292
+ };
1293
+ }
1294
+
1295
+ function arabicOnlyValidator() {
1296
+ return (control) => {
1297
+ if (!control.value) {
1298
+ return null;
1299
+ }
1300
+ return REGEX.ARABIC_ONLY.test(control.value) ? null : { arabicOnly: true };
1301
+ };
1302
+ }
1303
+
1304
+ function englishOnlyValidator() {
1305
+ return (control) => {
1306
+ if (!control.value) {
1307
+ return null;
1308
+ }
1309
+ return REGEX.ENGLISH_ONLY.test(control.value) ? null : { englishOnly: true };
1310
+ };
1311
+ }
1312
+
1313
+ function numbersOnlyValidator() {
1314
+ return (control) => {
1315
+ if (!control.value) {
1316
+ return null;
1317
+ }
1318
+ return REGEX.NUMBERS_ONLY.test(control.value) ? null : { numbersOnly: true };
1319
+ };
1320
+ }
1321
+
1322
+ function lettersOnlyValidator() {
1323
+ return (control) => {
1324
+ if (!control.value) {
1325
+ return null;
1326
+ }
1327
+ return REGEX.LETTERS_ONLY.test(control.value) ? null : { lettersOnly: true };
1328
+ };
1329
+ }
1330
+
1331
+ function alphanumericValidator() {
1332
+ return (control) => {
1333
+ if (!control.value) {
1334
+ return null;
1335
+ }
1336
+ return REGEX.ALPHANUMERIC.test(control.value) ? null : { alphanumericOnly: true };
1337
+ };
1338
+ }
1339
+
1340
+ function phoneValidator() {
1341
+ return (control) => {
1342
+ if (!control.value) {
1343
+ return null;
1344
+ }
1345
+ return REGEX.PHONE.test(control.value) ? null : { phone: true };
1346
+ };
1347
+ }
1348
+
1349
+ function urlValidator() {
1350
+ return (control) => {
1351
+ if (!control.value) {
1352
+ return null;
1353
+ }
1354
+ return REGEX.URL.test(control.value) ? null : { invalidUrl: true };
1355
+ };
1356
+ }
1357
+
1358
+ function ibanValidator() {
1359
+ return (control) => {
1360
+ if (!control.value) {
1361
+ return null;
1362
+ }
1363
+ const value = control.value.replace(REGEX.WHITESPACE_GLOBAL, '').toUpperCase();
1364
+ return REGEX.SAUDI_IBAN.test(value) ? null : { invalidIban: true };
1365
+ };
1366
+ }
1367
+
1368
+ /**
1369
+ * Parses a date string in DD/MM/YYYY format.
1370
+ * @returns Parsed Date or null if invalid.
1371
+ */
1372
+ function parseDate(value) {
1373
+ const parts = value.split('/');
1374
+ if (parts.length !== 3) {
1375
+ return null;
1376
+ }
1377
+ const day = +parts[0];
1378
+ const month = +parts[1] - 1;
1379
+ const year = +parts[2];
1380
+ const date = new Date(year, month, day);
1381
+ return isNaN(date.getTime()) ? null : date;
1382
+ }
1383
+
1384
+ /** Validates control date is >= minDate. Expects DD/MM/YYYY or Date. */
1385
+ function dateMinValidator(minDate) {
1386
+ return (control) => {
1387
+ if (!control.value) {
1388
+ return null;
1389
+ }
1390
+ const controlDate = parseDate(control.value);
1391
+ const min = typeof minDate === 'string' ? parseDate(minDate) : minDate;
1392
+ if (!controlDate || !min) {
1393
+ return { invalidDate: true };
1394
+ }
1395
+ const controlDay = new Date(controlDate.getFullYear(), controlDate.getMonth(), controlDate.getDate());
1396
+ const minDay = new Date(min.getFullYear(), min.getMonth(), min.getDate());
1397
+ return controlDay >= minDay ? null : { dateBeforeMin: true };
1398
+ };
1399
+ }
1400
+
1401
+ /** Validates control date is <= maxDate. Expects DD/MM/YYYY or Date. */
1402
+ function dateMaxValidator(maxDate) {
1403
+ return (control) => {
1404
+ if (!control.value) {
1405
+ return null;
1406
+ }
1407
+ const controlDate = parseDate(control.value);
1408
+ const max = typeof maxDate === 'string' ? parseDate(maxDate) : maxDate;
1409
+ if (!controlDate || !max) {
1410
+ return { invalidDate: true };
1411
+ }
1412
+ return controlDate <= max ? null : { dateAfterMax: true };
1413
+ };
1414
+ }
1415
+
1416
+ /** Fails when value is only whitespace (after trim). */
1417
+ function noWhitespaceValidator() {
1418
+ return (control) => {
1419
+ if (!control.value) {
1420
+ return null;
1421
+ }
1422
+ const hasWhitespace = control.value.trim().length === 0;
1423
+ return hasWhitespace ? { noWhitespace: true } : null;
1424
+ };
1425
+ }
1426
+
1427
+ function crNumberValidator() {
1428
+ return (control) => {
1429
+ if (!control.value) {
1430
+ return null;
1431
+ }
1432
+ return REGEX.CR_NUMBER.test(control.value) ? null : { invalidCrNumber: true };
1433
+ };
1434
+ }
1435
+
1436
+ /** Form-level validator: confirm field must match source field. */
1437
+ function confirmedValidator(sourceKey, confirmKey) {
1438
+ return (control) => {
1439
+ const source = control.get(sourceKey);
1440
+ const confirm = control.get(confirmKey);
1441
+ if (source && confirm && source.value !== confirm.value) {
1442
+ confirm.setErrors({ ...confirm.errors, confirmationMismatch: true });
1443
+ return { confirmationMismatch: true };
1444
+ }
1445
+ if (confirm?.hasError('confirmationMismatch')) {
1446
+ const { confirmationMismatch, ...otherErrors } = confirm.errors || {};
1447
+ const hasOtherErrors = Object.keys(otherErrors).length > 0;
1448
+ confirm.setErrors(hasOtherErrors ? otherErrors : null);
1449
+ }
1450
+ return null;
1451
+ };
1452
+ }
1453
+
1454
+ /** Validates control value as an email address. */
1455
+ function emailValidator() {
1456
+ return (control) => {
1457
+ if (!control.value) {
1458
+ return null;
1459
+ }
1460
+ return REGEX.EMAIL.test(control.value) ? null : { invalidEmail: true };
1461
+ };
1462
+ }
1463
+
1464
+ /** Returns a validator that fails when the control value does not match the given regex. */
1465
+ function patternValidator(pattern, errorKey) {
1466
+ return (control) => {
1467
+ if (!control.value) {
1468
+ return null;
1469
+ }
1470
+ return pattern.test(control.value) ? null : { [errorKey]: true };
1471
+ };
1472
+ }
1473
+
1474
+ function minSelectedValidator(min) {
1475
+ return (control) => {
1476
+ const value = control.value;
1477
+ if (Array.isArray(value) && value.length >= min) {
1478
+ return null;
1479
+ }
1480
+ return { minSelected: { min, actual: Array.isArray(value) ? value.length : 0 } };
1481
+ };
1482
+ }
1483
+
1484
+ /** Injection token for Environment. Prefer reading from `environments` after bootstrap. */
1485
+ const ENVIRONMENT = new InjectionToken('ENVIRONMENT');
1486
+
1487
+ const environments = {
1488
+ production: false,
1489
+ staging: false,
1490
+ apiUrl: '',
1491
+ sso: {
1492
+ enabled: false,
1493
+ issuer: '',
1494
+ loginUrl: '',
1495
+ tokenEndpoint: '',
1496
+ userinfoEndpoint: '',
1497
+ logoutUrl: '',
1498
+ jwksUri: '',
1499
+ clientId: '',
1500
+ redirectUri: '',
1501
+ postLogoutRedirectUri: '',
1502
+ },
1503
+ };
1504
+
1505
+ function provideEnvironment(environment) {
1506
+ return provideAppInitializer(() => {
1507
+ Object.assign(environments, environment);
1508
+ });
1509
+ }
1510
+
1511
+ /**
1512
+ * Syncs a form's disabled state with a loading signal.
1513
+ * Disables the form when loading is true, enables when false.
1514
+ * Must be called in an injection context (constructor or field initializer).
1515
+ */
1516
+ function syncFormWithLoading(form, loading) {
1517
+ effect(() => {
1518
+ if (loading()) {
1519
+ form.disable();
1520
+ }
1521
+ else {
1522
+ form.enable();
1523
+ }
1524
+ });
1525
+ }
1526
+ /**
1527
+ * Sets up an effect to patch form values when initialData changes.
1528
+ * Call from constructor in an injection context.
1529
+ */
1530
+ function useFormInitData(form, initialData) {
1531
+ effect(() => initFormData(form, initialData()));
1532
+ }
1533
+ /**
1534
+ * Patches form values from partial data. No-op when data is empty.
1535
+ * Safe to call with initialData from route or parent.
1536
+ */
1537
+ function initFormData(form, data) {
1538
+ if (data && Object.keys(data).length > 0) {
1539
+ form.patchValue(data);
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Marks form as submitted and touched. Returns true if valid, false otherwise.
1544
+ */
1545
+ function markSubmittedAndValidate(form) {
1546
+ form.markAllAsTouched();
1547
+ return form.valid;
1548
+ }
1549
+
1550
+ /** Updates document title and meta tags from route data and current language. */
1551
+ class SeoService {
1552
+ titleService = inject(Title);
1553
+ metaService = inject(Meta);
1554
+ router = inject(Router);
1555
+ activatedRoute = inject(ActivatedRoute);
1556
+ translate = inject(TranslateService);
1557
+ destroyRef = inject(DestroyRef);
1558
+ doc = inject(DOCUMENT);
1559
+ /** Subscribes to navigation and language changes to keep SEO tags in sync. */
1560
+ init() {
1561
+ merge(this.router.events.pipe(filter(event => event instanceof NavigationEnd)), this.translate.onLangChange)
1562
+ .pipe(takeUntilDestroyed(this.destroyRef))
1563
+ .subscribe(() => {
1564
+ this.updateSeo();
1565
+ });
1566
+ }
1567
+ updateSeo() {
1568
+ let route = this.activatedRoute;
1569
+ while (route.firstChild) {
1570
+ route = route.firstChild;
1571
+ }
1572
+ const data = route.snapshot.data;
1573
+ const appName = this.translate.instant('APP_NAME');
1574
+ const finalTitle = data.title ? `${this.translate.instant(data.title)} | ${appName}` : appName;
1575
+ let finalDesc = '';
1576
+ // Set Title & OG:Title
1577
+ this.titleService.setTitle(finalTitle);
1578
+ this.metaService.updateTag({ property: 'og:title', content: finalTitle });
1579
+ this.metaService.updateTag({ name: 'twitter:title', content: finalTitle });
1580
+ // Set Meta Tags (Description, Keywords)
1581
+ if (data.meta) {
1582
+ data.meta.forEach(tag => {
1583
+ if (tag.content) {
1584
+ const translatedContent = this.translate.instant(tag.content);
1585
+ this.metaService.updateTag({ ...tag, content: translatedContent });
1586
+ if (tag.name === 'description') {
1587
+ finalDesc = translatedContent;
1588
+ this.metaService.updateTag({ property: 'og:description', content: finalDesc });
1589
+ this.metaService.updateTag({ name: 'twitter:description', content: finalDesc });
1590
+ }
1591
+ }
1592
+ });
1593
+ }
1594
+ // Standard Meta
1595
+ this.metaService.updateTag({ name: 'robots', content: 'index, follow' });
1596
+ this.metaService.updateTag({ name: 'author', content: appName });
1597
+ this.metaService.updateTag({ property: 'og:type', content: 'website' });
1598
+ this.metaService.updateTag({ property: 'og:site_name', content: appName });
1599
+ this.metaService.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
1600
+ // URL & Image
1601
+ const url = this.doc.location.href;
1602
+ this.metaService.updateTag({ property: 'og:url', content: url });
1603
+ const origin = this.doc.location.origin;
1604
+ this.metaService.updateTag({ property: 'og:image', content: `${origin}/assets/images/og-image.png` });
1605
+ this.metaService.updateTag({ property: 'og:image:width', content: '1200' });
1606
+ this.metaService.updateTag({ property: 'og:image:height', content: '630' });
1607
+ this.metaService.updateTag({ name: 'twitter:image', content: `${origin}/assets/images/og-image.png` });
1608
+ // Keywords
1609
+ const keywords = this.translate.instant('KEYWORDS');
1610
+ if (keywords && keywords !== 'KEYWORDS') {
1611
+ this.metaService.updateTag({ name: 'keywords', content: keywords });
1612
+ }
1613
+ // Canonical
1614
+ this.createLinkForCanonicalURL(url);
1615
+ }
1616
+ createLinkForCanonicalURL(url) {
1617
+ const link = this.doc.querySelector("link[rel='canonical']") || this.doc.createElement('link');
1618
+ link.setAttribute('rel', 'canonical');
1619
+ link.setAttribute('href', url);
1620
+ this.doc.head.appendChild(link);
1621
+ }
1622
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1623
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SeoService, providedIn: 'root' });
1624
+ }
1625
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SeoService, decorators: [{
1626
+ type: Injectable,
1627
+ args: [{ providedIn: 'root' }]
1628
+ }] });
1629
+
1630
+ /**
1631
+ * localStorage key for the raw SSO callback params snapshot.
1632
+ * Saved before angular-oauth2-oidc's tryLogin() strips them from the URL.
1633
+ */
1634
+ const SSO_CALLBACK_PARAMS_KEY = 'ncim_sso_callback_params';
1635
+ /**
1636
+ * localStorage key for the intended destination URL before SSO login.
1637
+ * Written by loginWithSso(), read and cleared by getAndClearRedirectUrl().
1638
+ */
1639
+ const SSO_REDIRECT_URL_KEY = 'ncim_sso_redirect_url';
1640
+ /**
1641
+ * Wraps angular-oauth2-oidc for MOMRAH SSO (OIDC Authorization Code + PKCE).
1642
+ *
1643
+ * All OIDC endpoints are configured explicitly — no discovery document fetch —
1644
+ * to avoid CORS issues with the SSO server. strictDiscoveryDocumentValidation
1645
+ * is disabled so the library works without a discovery document fetch.
1646
+ *
1647
+ * Call initialize() once at app startup via APP_INITIALIZER (via provideSso()).
1648
+ */
1649
+ class OidcAuthService {
1650
+ oauthService = inject(OAuthService);
1651
+ env = inject(ENVIRONMENT);
1652
+ // APP_BASE_HREF reflects the <base href> at runtime (e.g. '/commercial-compliance/').
1653
+ // Optional because it may not be provided in tests.
1654
+ appBaseHref = inject(APP_BASE_HREF, { optional: true }) ?? '/';
1655
+ isAuthenticated = signal(false, ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
1656
+ currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
1657
+ /** True when SSO is enabled in the current environment. */
1658
+ get isSsoEnabled() {
1659
+ return this.env.sso.enabled;
1660
+ }
1661
+ /**
1662
+ * Configure OIDC, process a redirect callback if present, and start
1663
+ * automatic silent token refresh for session continuity.
1664
+ * No-op when sso.enabled=false (local dev without MOMRAH network access).
1665
+ */
1666
+ initialize() {
1667
+ if (!this.env.sso.enabled) {
1668
+ return Promise.resolve(false);
1669
+ }
1670
+ this.configure();
1671
+ // Snapshot callback params BEFORE tryLogin() strips them from the URL.
1672
+ this.snapshotCallbackParams();
1673
+ // Monitor token lifecycle events (refresh errors, session termination).
1674
+ this.listenToTokenEvents();
1675
+ return this.oauthService
1676
+ .tryLogin()
1677
+ .then(() => {
1678
+ this.syncState();
1679
+ if (this.isAuthenticated() && this.env.sso.silentRefreshEnabled) {
1680
+ this.oauthService.setupAutomaticSilentRefresh();
1681
+ }
1682
+ return this.isAuthenticated();
1683
+ })
1684
+ .catch((err) => {
1685
+ this.isAuthenticated.set(false);
1686
+ this.currentUser.set(null);
1687
+ if (!this.env.production && err) {
1688
+ console.warn('[OidcAuthService] SSO initialization failed:', err);
1689
+ }
1690
+ return false;
1691
+ });
1692
+ }
1693
+ /**
1694
+ * Redirect the browser to the MOMRAH SSO login page.
1695
+ * Pass `redirectUrl` to restore the user to a specific page after login.
1696
+ * Only relative app paths are accepted — external URLs are silently ignored
1697
+ * to prevent open redirect attacks.
1698
+ */
1699
+ loginWithSso(redirectUrl) {
1700
+ this.configure();
1701
+ if (redirectUrl && isSafeLocalPath(redirectUrl)) {
1702
+ localStorage.setItem(SSO_REDIRECT_URL_KEY, redirectUrl);
1703
+ }
1704
+ const customParams = this.env.sso.customQueryParams ?? {};
1705
+ this.oauthService.initCodeFlow(undefined, customParams);
1706
+ }
1707
+ /**
1708
+ * Build the OIDC RP-initiated logout URL (end-session endpoint) including
1709
+ * id_token_hint and post_logout_redirect_uri pointing to /auth/login.
1710
+ *
1711
+ * Must be called BEFORE logout() because logout() clears the stored id_token.
1712
+ * Returns null when SSO is disabled, not authenticated, or the id_token is gone.
1713
+ *
1714
+ * The returned URL should be assigned to window.location.href so the SSO
1715
+ * server can terminate the server-side session and redirect back to the app.
1716
+ */
1717
+ buildEndSessionUrl() {
1718
+ if (!this.env.sso.enabled || !this.env.sso.logoutUrl) {
1719
+ return null;
1720
+ }
1721
+ const idToken = this.oauthService.getIdToken();
1722
+ if (!idToken) {
1723
+ return null;
1724
+ }
1725
+ const origin = window.location.origin;
1726
+ const baseHref = this.appBaseHref.replace(/\/$/, '');
1727
+ const params = new URLSearchParams({
1728
+ id_token_hint: idToken,
1729
+ post_logout_redirect_uri: `${origin}${baseHref}${ROUTE_PATHS.AUTH_LOGIN}`,
1730
+ client_id: this.env.sso.clientId,
1731
+ });
1732
+ return `${this.env.sso.logoutUrl}?${params.toString()}`;
1733
+ }
1734
+ /**
1735
+ * Clear all local SSO tokens and state without redirecting to the SSO
1736
+ * end-session endpoint. The caller handles navigation (see buildEndSessionUrl).
1737
+ */
1738
+ logout() {
1739
+ this.isAuthenticated.set(false);
1740
+ this.currentUser.set(null);
1741
+ localStorage.removeItem(STORAGE_KEYS.USER_PROFILE);
1742
+ localStorage.removeItem(SSO_CALLBACK_PARAMS_KEY);
1743
+ localStorage.removeItem(SSO_REDIRECT_URL_KEY);
1744
+ // noRedirectToLogoutUrl=true: clears OIDC tokens without a browser redirect.
1745
+ this.oauthService.logOut(true);
1746
+ }
1747
+ /** Returns the current OIDC access token, or null if not authenticated via SSO. */
1748
+ getAccessToken() {
1749
+ return this.oauthService.getAccessToken() || null;
1750
+ }
1751
+ /**
1752
+ * Returns the saved post-login redirect URL and removes it from localStorage.
1753
+ * Returns null if no redirect URL was stored.
1754
+ */
1755
+ getAndClearRedirectUrl() {
1756
+ const url = localStorage.getItem(SSO_REDIRECT_URL_KEY);
1757
+ if (url) {
1758
+ localStorage.removeItem(SSO_REDIRECT_URL_KEY);
1759
+ }
1760
+ return url;
1761
+ }
1762
+ /**
1763
+ * Fetch fresh user attributes from the UserInfo endpoint and sync local state.
1764
+ * Call this after the user updates their profile on the MOMRAH SSO server,
1765
+ * as per spec: "the outcome of the update profile scenario is a renewed JWT
1766
+ * Token via the User Info Endpoint."
1767
+ */
1768
+ refreshUserInfo() {
1769
+ if (!this.env.sso.enabled || !this.isAuthenticated()) {
1770
+ return Promise.resolve();
1771
+ }
1772
+ return this.oauthService
1773
+ .loadUserProfile()
1774
+ .then(() => {
1775
+ this.syncState();
1776
+ })
1777
+ .catch(() => {
1778
+ // Non-fatal — keep existing cached claims on failure.
1779
+ });
1780
+ }
1781
+ configure() {
1782
+ const sso = this.env.sso;
1783
+ const origin = window.location.origin;
1784
+ // Strip trailing slash: '/commercial-compliance/' → '/commercial-compliance'
1785
+ const baseHref = this.appBaseHref.replace(/\/$/, '');
1786
+ const config = {
1787
+ issuer: sso.issuer,
1788
+ loginUrl: sso.loginUrl,
1789
+ tokenEndpoint: sso.tokenEndpoint,
1790
+ userinfoEndpoint: sso.userinfoEndpoint,
1791
+ logoutUrl: sso.logoutUrl,
1792
+ clientId: sso.clientId,
1793
+ // Include base href so redirectUri matches what the SSO server has registered.
1794
+ // local (baseHref='/') → http://localhost:4200/sso-callback
1795
+ // production (baseHref='/commercial-compliance/') → …/commercial-compliance/sso-callback
1796
+ redirectUri: sso.redirectUri ?? `${origin}${baseHref}${ROUTE_PATHS.SSO_CALLBACK}`,
1797
+ postLogoutRedirectUri: sso.postLogoutRedirectUri ?? `${origin}${baseHref}${ROUTE_PATHS.AUTH_LOGIN}`,
1798
+ responseType: 'code',
1799
+ // Use env-configured scopes if provided; fall back to spec minimum.
1800
+ scope: sso.scope ?? 'openid profile email',
1801
+ // Skip discovery document for issuer validation — endpoints are explicit above.
1802
+ skipIssuerCheck: true,
1803
+ strictDiscoveryDocumentValidation: false,
1804
+ showDebugInformation: !this.env.production,
1805
+ requireHttps: 'remoteOnly', // allow http://localhost in dev
1806
+ clearHashAfterLogin: false,
1807
+ useSilentRefresh: sso.silentRefreshEnabled ?? false,
1808
+ timeoutFactor: 0.75, // refresh at 75% of token lifetime
1809
+ };
1810
+ this.oauthService.configure(config);
1811
+ }
1812
+ /**
1813
+ * Save all OIDC callback query params to localStorage before the library
1814
+ * strips them from the URL via history.replaceState. The SsoCallback component
1815
+ * reads this snapshot to restore the query string.
1816
+ */
1817
+ snapshotCallbackParams() {
1818
+ const params = new URLSearchParams(window.location.search);
1819
+ if (!params.has('code') || !params.has('state')) {
1820
+ return;
1821
+ }
1822
+ const snapshot = {};
1823
+ params.forEach((value, key) => (snapshot[key] = value));
1824
+ localStorage.setItem(SSO_CALLBACK_PARAMS_KEY, JSON.stringify(snapshot));
1825
+ }
1826
+ /**
1827
+ * Subscribe to angular-oauth2-oidc lifecycle events.
1828
+ * Handles token refresh failures and session termination by clearing local auth state.
1829
+ * On silent refresh success, re-syncs the state signal.
1830
+ */
1831
+ listenToTokenEvents() {
1832
+ this.oauthService.events.subscribe(event => {
1833
+ switch (event.type) {
1834
+ case 'token_refresh_error':
1835
+ case 'session_error':
1836
+ case 'session_terminated':
1837
+ // Silent refresh failed or SSO session ended — clear local state.
1838
+ this.isAuthenticated.set(false);
1839
+ this.currentUser.set(null);
1840
+ localStorage.removeItem(STORAGE_KEYS.USER_PROFILE);
1841
+ break;
1842
+ case 'token_received':
1843
+ // New token received after a silent refresh — re-sync.
1844
+ this.syncState();
1845
+ break;
1846
+ default:
1847
+ break;
1848
+ }
1849
+ });
1850
+ }
1851
+ syncState() {
1852
+ const authenticated = this.oauthService.hasValidAccessToken();
1853
+ this.isAuthenticated.set(authenticated);
1854
+ if (authenticated) {
1855
+ const claims = this.oauthService.getIdentityClaims();
1856
+ const user = toSsoUser(claims);
1857
+ this.currentUser.set(user);
1858
+ // Persist user profile to localStorage so components can read it
1859
+ // synchronously on page load before the signal is populated.
1860
+ if (user) {
1861
+ localStorage.setItem(STORAGE_KEYS.USER_PROFILE, JSON.stringify(user));
1862
+ }
1863
+ }
1864
+ else {
1865
+ localStorage.removeItem(STORAGE_KEYS.USER_PROFILE);
1866
+ }
1867
+ }
1868
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OidcAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1869
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OidcAuthService, providedIn: 'root' });
1870
+ }
1871
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OidcAuthService, decorators: [{
1872
+ type: Injectable,
1873
+ args: [{ providedIn: 'root' }]
1874
+ }] });
1875
+ /**
1876
+ * Validates OIDC identity claims and narrows to SsoUser.
1877
+ * Requires at minimum a non-empty `sub` claim — the OIDC subject identifier.
1878
+ * Returns null if claims are missing or malformed.
1879
+ */
1880
+ function toSsoUser(claims) {
1881
+ if (!claims || typeof claims !== 'object') {
1882
+ return null;
1883
+ }
1884
+ const c = claims;
1885
+ if (typeof c['sub'] !== 'string' || !c['sub']) {
1886
+ return null;
1887
+ }
1888
+ return c;
1889
+ }
1890
+ /**
1891
+ * Returns true only for safe local (relative) app paths.
1892
+ * Rejects external URLs, protocol-relative URLs, and anything that could
1893
+ * redirect the user outside the app after SSO login.
1894
+ */
1895
+ function isSafeLocalPath(url) {
1896
+ return url.startsWith('/') && !url.startsWith('//') && !url.includes('://');
1897
+ }
1898
+
1899
+ const CACHE_ENABLED = new HttpContextToken(() => true);
1900
+ const CACHE_TTL = new HttpContextToken(() => CACHE_DEFAULTS.TTL_MS);
1901
+ class HttpCacheService {
1902
+ store = new Map();
1903
+ size = signal(0, ...(ngDevMode ? [{ debugName: "size" }] : []));
1904
+ get(url, ttl = CACHE_DEFAULTS.TTL_MS) {
1905
+ const entry = this.store.get(url);
1906
+ if (!entry) {
1907
+ return null;
1908
+ }
1909
+ if (Date.now() - entry.timestamp > ttl) {
1910
+ this.store.delete(url);
1911
+ this.syncSize();
1912
+ return null;
1913
+ }
1914
+ this.store.delete(url);
1915
+ this.store.set(url, entry);
1916
+ return entry.response;
1917
+ }
1918
+ set(url, response) {
1919
+ if (this.store.size >= CACHE_DEFAULTS.MAX_SIZE) {
1920
+ const oldestKey = this.store.keys().next().value;
1921
+ if (oldestKey) {
1922
+ this.store.delete(oldestKey);
1923
+ }
1924
+ }
1925
+ this.store.set(url, { response, timestamp: Date.now() });
1926
+ this.syncSize();
1927
+ }
1928
+ clear(url) {
1929
+ if (url) {
1930
+ this.store.delete(url);
1931
+ }
1932
+ else {
1933
+ this.store.clear();
1934
+ }
1935
+ this.syncSize();
1936
+ }
1937
+ clearPattern(pattern) {
1938
+ let deleted = false;
1939
+ for (const key of this.store.keys()) {
1940
+ if (pattern.test(key)) {
1941
+ this.store.delete(key);
1942
+ deleted = true;
1943
+ }
1944
+ }
1945
+ if (deleted) {
1946
+ this.syncSize();
1947
+ }
1948
+ }
1949
+ syncSize() {
1950
+ this.size.set(this.store.size);
1951
+ }
1952
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HttpCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1953
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HttpCacheService, providedIn: 'root' });
1954
+ }
1955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HttpCacheService, decorators: [{
1956
+ type: Injectable,
1957
+ args: [{ providedIn: 'root' }]
1958
+ }] });
1959
+
1960
+ class LoadingService {
1961
+ globalCounter = signal(0, ...(ngDevMode ? [{ debugName: "globalCounter" }] : []));
1962
+ operations = signal(new Set(), ...(ngDevMode ? [{ debugName: "operations" }] : []));
1963
+ isLoading = computed(() => this.globalCounter() > 0, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1964
+ count = this.globalCounter.asReadonly();
1965
+ activeOperations = this.operations.asReadonly();
1966
+ show() {
1967
+ this.globalCounter.update(n => n + 1);
1968
+ }
1969
+ hide() {
1970
+ this.globalCounter.update(n => Math.max(0, n - 1));
1971
+ }
1972
+ reset() {
1973
+ this.globalCounter.set(0);
1974
+ }
1975
+ start(key) {
1976
+ this.operations.update(set => new Set(set).add(key));
1977
+ }
1978
+ stop(key) {
1979
+ this.operations.update(set => {
1980
+ const next = new Set(set);
1981
+ next.delete(key);
1982
+ return next;
1983
+ });
1984
+ }
1985
+ isActive(key) {
1986
+ return this.operations().has(key);
1987
+ }
1988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LoadingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1989
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LoadingService, providedIn: 'root' });
1990
+ }
1991
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LoadingService, decorators: [{
1992
+ type: Injectable,
1993
+ args: [{ providedIn: 'root' }]
1994
+ }] });
1995
+
1996
+ const SKIP_LOADING = new HttpContextToken(() => false);
1997
+ const loadingInterceptor = (req, next) => {
1998
+ if (req.context.get(SKIP_LOADING)) {
1999
+ return next(req);
2000
+ }
2001
+ const loading = inject(LoadingService);
2002
+ loading.show();
2003
+ return next(req).pipe(finalize(() => loading.hide()));
2004
+ };
2005
+
2006
+ const SKIP_RETRY = new HttpContextToken(() => false);
2007
+ const MAX_RETRY_COUNT = new HttpContextToken(() => RETRY_DEFAULTS.MAX_COUNT);
2008
+ const retryInterceptor = (req, next) => {
2009
+ if (req.context.get(SKIP_RETRY)) {
2010
+ return next(req);
2011
+ }
2012
+ return next(req).pipe(retry({
2013
+ count: req.context.get(MAX_RETRY_COUNT),
2014
+ delay: (error, retryCount) => {
2015
+ const isRetryable = error instanceof HttpErrorResponse &&
2016
+ IDEMPOTENT_METHODS.has(req.method) &&
2017
+ RETRYABLE_STATUS_CODES.has(error.status);
2018
+ if (!isRetryable) {
2019
+ throw error;
2020
+ }
2021
+ return timer(Math.pow(2, retryCount - 1) * RETRY_DEFAULTS.BACKOFF_BASE_MS);
2022
+ },
2023
+ }));
2024
+ };
2025
+
2026
+ /** Set to true to skip attaching the Authorization header on a request. */
2027
+ const SKIP_AUTH = new HttpContextToken(() => false);
2028
+
2029
+ /** Set to true to skip the global error interceptor toast handling for a request. */
2030
+ const SKIP_ERROR_HANDLING = new HttpContextToken(() => false);
2031
+
2032
+ class ApiService {
2033
+ http = inject(HttpClient);
2034
+ env = inject(ENVIRONMENT);
2035
+ get(path, options) {
2036
+ return this.http.get(this.resolveUrl(path, options), {
2037
+ ...this.buildHttpOptions(options),
2038
+ responseType: options?.responseType || 'json',
2039
+ observe: options?.observe,
2040
+ });
2041
+ }
2042
+ post(path, body, options) {
2043
+ return this.http.post(this.resolveUrl(path, options), body, {
2044
+ ...this.buildHttpOptions(options),
2045
+ responseType: options?.responseType || 'json',
2046
+ observe: options?.observe,
2047
+ });
2048
+ }
2049
+ put(path, body, options) {
2050
+ return this.http.put(this.resolveUrl(path, options), body, {
2051
+ ...this.buildHttpOptions(options),
2052
+ responseType: options?.responseType || 'json',
2053
+ observe: options?.observe,
2054
+ });
2055
+ }
2056
+ patch(path, body, options) {
2057
+ return this.http.patch(this.resolveUrl(path, options), body, {
2058
+ ...this.buildHttpOptions(options),
2059
+ responseType: options?.responseType || 'json',
2060
+ observe: options?.observe,
2061
+ });
2062
+ }
2063
+ delete(path, options) {
2064
+ return this.http.delete(this.resolveUrl(path, options), {
2065
+ ...this.buildHttpOptions(options),
2066
+ responseType: options?.responseType || 'json',
2067
+ observe: options?.observe,
2068
+ });
2069
+ }
2070
+ upload(path, formData, options) {
2071
+ return this.http.post(this.resolveUrl(path, options), formData, {
2072
+ ...this.buildHttpOptions({ skipRetry: true, skipCache: true, ...options }),
2073
+ reportProgress: true,
2074
+ observe: 'events',
2075
+ });
2076
+ }
2077
+ download(path, options) {
2078
+ return this.http.get(this.resolveUrl(path, options), {
2079
+ ...this.buildHttpOptions({ skipCache: true, ...options }),
2080
+ responseType: 'blob',
2081
+ });
2082
+ }
2083
+ resolveUrl(path, options) {
2084
+ return options?.fullUrl ? path : `${this.env.apiUrl}${path}`;
2085
+ }
2086
+ buildHttpOptions(options) {
2087
+ const ctx = new HttpContext();
2088
+ if (options?.skipLoading) {
2089
+ ctx.set(SKIP_LOADING, true);
2090
+ }
2091
+ if (options?.skipCache) {
2092
+ ctx.set(CACHE_ENABLED, false);
2093
+ }
2094
+ if (options?.cacheTTL != null) {
2095
+ ctx.set(CACHE_TTL, options.cacheTTL);
2096
+ }
2097
+ if (options?.skipRetry) {
2098
+ ctx.set(SKIP_RETRY, true);
2099
+ }
2100
+ if (options?.skipAuth) {
2101
+ ctx.set(SKIP_AUTH, true);
2102
+ }
2103
+ if (options?.skipErrorHandling) {
2104
+ ctx.set(SKIP_ERROR_HANDLING, true);
2105
+ }
2106
+ return { params: options?.params, headers: options?.headers, context: ctx };
2107
+ }
2108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2109
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ApiService, providedIn: 'root' });
2110
+ }
2111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ApiService, decorators: [{
2112
+ type: Injectable,
2113
+ args: [{ providedIn: 'root' }]
2114
+ }] });
2115
+
2116
+ /** Facade that unifies OIDC (MOMRAH SSO) and standard JWT authentication. */
2117
+ class AuthService {
2118
+ oidc = inject(OidcAuthService);
2119
+ api = inject(ApiService);
2120
+ /** True when a standard JWT is stored locally (non-SSO login). */
2121
+ _jwtAuthenticated = signal(this.checkJwt(), ...(ngDevMode ? [{ debugName: "_jwtAuthenticated" }] : []));
2122
+ /** Decoded payload from the stored backend JWT — populated by login(), cleared by logout(). */
2123
+ _jwtUser = signal(this.readJwtUser(), ...(ngDevMode ? [{ debugName: "_jwtUser" }] : []));
2124
+ /** True if the user is authenticated via SSO or standard JWT. */
2125
+ isAuthenticated = computed(() => this.oidc.isAuthenticated() || this._jwtAuthenticated(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
2126
+ identityNumber = signal(localStorage.getItem(STORAGE_KEYS.NAFATH_IDENTITY) ?? '', ...(ngDevMode ? [{ debugName: "identityNumber" }] : []));
2127
+ /** True when SSO is enabled in the current environment. */
2128
+ get isSsoEnabled() {
2129
+ return this.oidc.isSsoEnabled;
2130
+ }
2131
+ /** SSO user claims from the OIDC identity token. Null for standard JWT logins. */
2132
+ currentUser = computed(() => this.oidc.currentUser(), ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
2133
+ /** Decoded JWT payload for standard (non-SSO) logins. Null for SSO logins. */
2134
+ jwtUser = computed(() => this._jwtUser(), ...(ngDevMode ? [{ debugName: "jwtUser" }] : []));
2135
+ // ─── SSO Flow ────────────────────────────────────────────────────────────────
2136
+ /**
2137
+ * Redirect to the MOMRAH SSO login page.
2138
+ * Pass `redirectUrl` to restore the user to a specific page after login.
2139
+ */
2140
+ loginWithSso(redirectUrl) {
2141
+ this.oidc.loginWithSso(redirectUrl);
2142
+ }
2143
+ /**
2144
+ * Returns the saved post-login redirect URL and removes it from localStorage.
2145
+ * Call this in the SSO callback component after successful login.
2146
+ */
2147
+ getAndClearRedirectUrl() {
2148
+ return this.oidc.getAndClearRedirectUrl();
2149
+ }
2150
+ /**
2151
+ * Exchange the OIDC access token for a backend JWT and mark the session as authenticated.
2152
+ * Call this from the SSO callback component after OIDC has processed the authorization code.
2153
+ * Emits once on success; errors if the OIDC token is missing or the backend rejects it.
2154
+ */
2155
+ completeSsoCallback() {
2156
+ const accessToken = this.oidc.getAccessToken();
2157
+ if (!accessToken) {
2158
+ return throwError(() => new Error('No OIDC access token — code exchange may have failed'));
2159
+ }
2160
+ return this.api
2161
+ .post(API_ENDPOINTS.AUTH.SSO_CALLBACK, { accessToken }, {
2162
+ skipAuth: true,
2163
+ skipErrorHandling: true, // SsoCallback handles errors by redirecting to /sso-error
2164
+ })
2165
+ .pipe(tap(response => this.login(response.token)), map(() => void 0));
2166
+ }
2167
+ /**
2168
+ * Build the OIDC RP-initiated logout URL for SSO end-session.
2169
+ * Must be called BEFORE logout() because logout() clears the stored id_token.
2170
+ * Returns null when SSO is disabled, not authenticated via SSO, or id_token is missing.
2171
+ */
2172
+ buildEndSessionUrl() {
2173
+ return this.oidc.buildEndSessionUrl();
2174
+ }
2175
+ /**
2176
+ * Fetch fresh user attributes from the MOMRAH SSO UserInfo endpoint and
2177
+ * sync the `currentUser` signal with updated claims.
2178
+ *
2179
+ * Per spec: "the outcome of the update profile scenario is a renewed JWT
2180
+ * Token via the User Info Endpoint."
2181
+ *
2182
+ * Call this after redirecting the user back from the SSO profile update page.
2183
+ */
2184
+ refreshUserInfo() {
2185
+ return this.oidc.refreshUserInfo();
2186
+ }
2187
+ // ─── Standard JWT Flow ───────────────────────────────────────────────────────
2188
+ /**
2189
+ * Store a backend JWT, decode its payload, and mark the session as authenticated.
2190
+ * Decoded user data is saved to USER_PROFILE for synchronous reads on page reload.
2191
+ * Real permissions/roles from the token replace any previously stored values.
2192
+ */
2193
+ login(token) {
2194
+ localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token);
2195
+ const payload = decodeJwtPayload(token);
2196
+ // Persist real permissions/roles from the token payload.
2197
+ const permissions = payload?.permissions ?? payload?.roles ?? [];
2198
+ localStorage.setItem(STORAGE_KEYS.PERMISSIONS, JSON.stringify(permissions));
2199
+ // Persist decoded user profile so components can read synchronously on reload.
2200
+ if (payload) {
2201
+ localStorage.setItem(STORAGE_KEYS.USER_PROFILE, JSON.stringify(payload));
2202
+ this._jwtUser.set(payload);
2203
+ }
2204
+ this._jwtAuthenticated.set(true);
2205
+ }
2206
+ /**
2207
+ * Initiate login via Nafath using the user's ID number.
2208
+ * Backend endpoint: /user/api/usersmanagement/Auth/LoginViaNafath
2209
+ */
2210
+ loginViaNafath(idNumber) {
2211
+ return this.api
2212
+ .post('/user/api/usersmanagement/Auth/LoginViaNafath', { idNumber }, {
2213
+ skipAuth: true,
2214
+ fullUrl: true,
2215
+ })
2216
+ .pipe(tap(resp => {
2217
+ const accessToken = resp?.value?.accessToken;
2218
+ if (!accessToken) {
2219
+ return;
2220
+ }
2221
+ // Store access token using the existing JWT login flow.
2222
+ this.login(accessToken);
2223
+ const refreshToken = resp?.value?.refreshToken;
2224
+ if (refreshToken) {
2225
+ localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
2226
+ }
2227
+ }), map(() => void 0));
2228
+ }
2229
+ // ─── Shared ───────────────────────────────────────────────────────────────────
2230
+ /** Clear all session data (SSO and JWT). Navigation is handled by the caller. */
2231
+ logout() {
2232
+ localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
2233
+ localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
2234
+ localStorage.removeItem(STORAGE_KEYS.PERMISSIONS);
2235
+ localStorage.removeItem(STORAGE_KEYS.USER_PROFILE);
2236
+ // Clear SSO callback param snapshot and saved redirect URL.
2237
+ localStorage.removeItem(SSO_CALLBACK_PARAMS_KEY);
2238
+ localStorage.removeItem(SSO_REDIRECT_URL_KEY);
2239
+ localStorage.removeItem(STORAGE_KEYS.NAFATH_IDENTITY);
2240
+ this.identityNumber.set('');
2241
+ this._jwtAuthenticated.set(false);
2242
+ this._jwtUser.set(null);
2243
+ // Always clear OIDC tokens — covers cases where the access token has
2244
+ // expired but the id_token is still in storage (oidc.isAuthenticated()
2245
+ // would return false but stale tokens would remain without this call).
2246
+ this.oidc.logout();
2247
+ }
2248
+ /** Returns the active access token (OIDC takes precedence over JWT). */
2249
+ getAccessToken() {
2250
+ return this.oidc.getAccessToken() ?? localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
2251
+ }
2252
+ /** Returns true if the user has all of the given permission(s). */
2253
+ hasPermission(permission) {
2254
+ if (!this.isAuthenticated()) {
2255
+ return false;
2256
+ }
2257
+ const stored = localStorage.getItem(STORAGE_KEYS.PERMISSIONS);
2258
+ const permissions = stored ? JSON.parse(stored) : [];
2259
+ const required = Array.isArray(permission) ? permission : [permission];
2260
+ return required.every(p => permissions.includes(p));
2261
+ }
2262
+ checkJwt() {
2263
+ return !!localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
2264
+ }
2265
+ /** Read and decode the stored JWT on service init to hydrate the jwtUser signal. */
2266
+ readJwtUser() {
2267
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
2268
+ return token ? decodeJwtPayload(token) : null;
2269
+ }
2270
+ /** Store the national identity number from Nafath login. */
2271
+ setIdentityNumber(id) {
2272
+ localStorage.setItem(STORAGE_KEYS.NAFATH_IDENTITY, id);
2273
+ this.identityNumber.set(id);
2274
+ }
2275
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2276
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthService, providedIn: 'root' });
2277
+ }
2278
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthService, decorators: [{
2279
+ type: Injectable,
2280
+ args: [{ providedIn: 'root' }]
2281
+ }] });
2282
+ /**
2283
+ * Decodes the payload segment of a JWT without verifying the signature.
2284
+ * Uses base64url decoding (replaces - with + and _ with /) then JSON.parse.
2285
+ * Returns null if the token is malformed or the payload cannot be parsed.
2286
+ */
2287
+ function decodeJwtPayload(token) {
2288
+ try {
2289
+ const parts = token.split('.');
2290
+ if (parts.length !== 3) {
2291
+ return null;
2292
+ }
2293
+ // Base64url → standard base64, then pad to a multiple of 4.
2294
+ const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
2295
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
2296
+ // Decode bytes → percent-encoded UTF-8 → Unicode string.
2297
+ const json = decodeURIComponent(atob(padded)
2298
+ .split('')
2299
+ .map(c => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`)
2300
+ .join(''));
2301
+ return JSON.parse(json);
2302
+ }
2303
+ catch {
2304
+ return null;
2305
+ }
2306
+ }
2307
+
2308
+ /**
2309
+ * Auth domain models.
2310
+ * SsoUser — OIDC identity claims from the MOMRAH SSO ID token.
2311
+ * JwtPayload — decoded payload from the backend-issued JWT.
2312
+ */
2313
+
2314
+ /**
2315
+ * Converts a flat or nested object into `HttpParams`.
2316
+ *
2317
+ * - `null` / `undefined` values are skipped.
2318
+ * - `Date` values are serialized to ISO-8601 strings.
2319
+ * - Arrays produce repeated keys: `{ ids: [1,2] }` → `ids=1&ids=2`.
2320
+ * - Nested objects are flattened with bracket notation: `{ filter: { name: 'x' } }` → `filter[name]=x`.
2321
+ */
2322
+ function toHttpParams(obj) {
2323
+ let params = new HttpParams();
2324
+ for (const [key, value] of Object.entries(obj)) {
2325
+ if (value == null) {
2326
+ continue;
2327
+ }
2328
+ if (isPlainObject(value)) {
2329
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
2330
+ params = appendParam(params, `${key}[${nestedKey}]`, nestedValue);
2331
+ }
2332
+ }
2333
+ else {
2334
+ params = appendParam(params, key, value);
2335
+ }
2336
+ }
2337
+ return params;
2338
+ }
2339
+ /**
2340
+ * Converts a flat or nested object into a `FormData` instance.
2341
+ *
2342
+ * - `null` / `undefined` values are skipped.
2343
+ * - `File` and `Blob` values are appended as-is.
2344
+ * - `Date` values are serialized to ISO-8601 strings.
2345
+ * - Arrays produce repeated keys: `{ tags: ['a','b'] }` → two `tags` entries.
2346
+ * - Nested objects are flattened with bracket notation: `{ address: { city: 'x' } }` → `address[city]=x`.
2347
+ */
2348
+ function toFormData(obj) {
2349
+ const fd = new FormData();
2350
+ appendToFormData(fd, obj, '');
2351
+ return fd;
2352
+ }
2353
+ function appendParam(params, key, value) {
2354
+ if (value == null) {
2355
+ return params;
2356
+ }
2357
+ if (Array.isArray(value)) {
2358
+ for (const item of value) {
2359
+ if (item != null) {
2360
+ params = params.append(key, serializePrimitive(item));
2361
+ }
2362
+ }
2363
+ return params;
2364
+ }
2365
+ return params.append(key, value instanceof Date ? value.toISOString() : String(value));
2366
+ }
2367
+ function appendToFormData(fd, data, prefix) {
2368
+ for (const [key, value] of Object.entries(data)) {
2369
+ const fullKey = prefix ? `${prefix}[${key}]` : key;
2370
+ if (value == null) {
2371
+ continue;
2372
+ }
2373
+ if (value instanceof File || value instanceof Blob) {
2374
+ fd.append(fullKey, value);
2375
+ }
2376
+ else if (value instanceof Date) {
2377
+ fd.append(fullKey, value.toISOString());
2378
+ }
2379
+ else if (Array.isArray(value)) {
2380
+ for (const item of value) {
2381
+ if (item == null) {
2382
+ continue;
2383
+ }
2384
+ if (item instanceof File || item instanceof Blob) {
2385
+ fd.append(fullKey, item);
2386
+ }
2387
+ else if (isPlainObject(item)) {
2388
+ appendToFormData(fd, item, fullKey);
2389
+ }
2390
+ else {
2391
+ fd.append(fullKey, String(item));
2392
+ }
2393
+ }
2394
+ }
2395
+ else if (isPlainObject(value)) {
2396
+ appendToFormData(fd, value, fullKey);
2397
+ }
2398
+ else {
2399
+ fd.append(fullKey, String(value));
2400
+ }
2401
+ }
2402
+ }
2403
+ function serializePrimitive(value) {
2404
+ return String(value);
2405
+ }
2406
+ function isPlainObject(value) {
2407
+ return (typeof value === 'object' &&
2408
+ value !== null &&
2409
+ !(value instanceof Date) &&
2410
+ !(value instanceof File) &&
2411
+ !(value instanceof Blob) &&
2412
+ !Array.isArray(value));
2413
+ }
2414
+
2415
+ /**
2416
+ * Tries to extract a human-readable message from common API error shapes:
2417
+ * { message: "..." } | { error: "..." } | { detail: "..." }
2418
+ * { error: { message: "..." } } | "plain string body"
2419
+ * { errors: [{ description/message/... }] }
2420
+ * { validationErrors: [...] } | { fieldErrors: {...} }
2421
+ */
2422
+ function extractApiErrorMessage(error) {
2423
+ const body = error.error;
2424
+ // Plain string body — truncate long values; skip raw HTML pages
2425
+ if (typeof body === 'string' && body.length > 0 && !body.trimStart().startsWith('<')) {
2426
+ return truncate$1(body, ERROR_MESSAGE_MAX_LENGTH);
2427
+ }
2428
+ if (!body || typeof body !== 'object') {
2429
+ return null;
2430
+ }
2431
+ const bodyObj = body;
2432
+ // errors / validationErrors / fieldErrors aliases
2433
+ const errors = bodyObj['errors'] ?? bodyObj['validationErrors'] ?? bodyObj['fieldErrors'];
2434
+ // Plain string errors field
2435
+ if (typeof errors === 'string' && errors.length > 0) {
2436
+ return truncate$1(errors, ERROR_MESSAGE_MAX_LENGTH);
2437
+ }
2438
+ // Array of error objects — mutually exclusive with object branch (fixes double-processing bug)
2439
+ if (Array.isArray(errors)) {
2440
+ for (const entry of errors) {
2441
+ if (!isObject(entry)) {
2442
+ continue;
2443
+ }
2444
+ const msg = extractMessageFromObject(entry);
2445
+ if (msg) {
2446
+ return truncate$1(msg, ERROR_MESSAGE_MAX_LENGTH);
2447
+ }
2448
+ }
2449
+ }
2450
+ else if (isObject(errors)) {
2451
+ for (const value of Object.values(errors)) {
2452
+ if (Array.isArray(value)) {
2453
+ for (const item of value) {
2454
+ if (typeof item === 'string' && item.length > 0) {
2455
+ return truncate$1(item, ERROR_MESSAGE_MAX_LENGTH);
2456
+ }
2457
+ if (isObject(item)) {
2458
+ const msg = extractMessageFromObject(item);
2459
+ if (msg) {
2460
+ return truncate$1(msg, ERROR_MESSAGE_MAX_LENGTH);
2461
+ }
2462
+ }
2463
+ }
2464
+ }
2465
+ else if (isObject(value)) {
2466
+ const msg = extractMessageFromObject(value);
2467
+ if (msg) {
2468
+ return truncate$1(msg, ERROR_MESSAGE_MAX_LENGTH);
2469
+ }
2470
+ }
2471
+ }
2472
+ }
2473
+ // Top-level message fields as fallback
2474
+ const directMessage = extractMessageFromObject(bodyObj);
2475
+ if (directMessage) {
2476
+ return truncate$1(directMessage, ERROR_MESSAGE_MAX_LENGTH);
2477
+ }
2478
+ return null;
2479
+ }
2480
+ /**
2481
+ * Extracts per-field validation errors from 422 response bodies.
2482
+ * Returns a formatted multi-line string or `null` if no field errors found.
2483
+ */
2484
+ function extractFieldErrors(error) {
2485
+ const body = error.error;
2486
+ const errorsObj = body?.errors ?? body?.validationErrors ?? body?.fieldErrors;
2487
+ if (!errorsObj || typeof errorsObj !== 'object') {
2488
+ return null;
2489
+ }
2490
+ const messages = flattenFieldMessages(errorsObj);
2491
+ if (messages.length === 0) {
2492
+ return null;
2493
+ }
2494
+ const visible = messages.slice(0, MAX_FIELD_ERRORS_SHOWN);
2495
+ const overflow = messages.length - MAX_FIELD_ERRORS_SHOWN;
2496
+ return overflow > 0 ? `${visible.join('\n')}\n… +${overflow}` : visible.join('\n');
2497
+ }
2498
+ function flattenFieldMessages(errors) {
2499
+ if (Array.isArray(errors)) {
2500
+ return errors
2501
+ .map(entry => {
2502
+ if (typeof entry === 'string') {
2503
+ return entry;
2504
+ }
2505
+ if (isObject(entry)) {
2506
+ // description added alongside message/msg/error
2507
+ const msg = entry['description'] ??
2508
+ entry['message'] ??
2509
+ entry['msg'] ??
2510
+ entry['error'];
2511
+ const field = entry['field'] ??
2512
+ entry['path'] ??
2513
+ entry['param'];
2514
+ if (typeof msg === 'string') {
2515
+ return field ? `${field}: ${msg}` : msg;
2516
+ }
2517
+ }
2518
+ return '';
2519
+ })
2520
+ .filter(Boolean);
2521
+ }
2522
+ return Object.entries(errors).flatMap(([field, value]) => {
2523
+ if (Array.isArray(value)) {
2524
+ // Handle both string[] and object[] array values
2525
+ return value.flatMap(v => {
2526
+ if (typeof v === 'string') {
2527
+ return [`${field}: ${v}`];
2528
+ }
2529
+ if (isObject(v)) {
2530
+ const msg = extractMessageFromObject(v);
2531
+ if (msg) {
2532
+ return [`${field}: ${msg}`];
2533
+ }
2534
+ }
2535
+ return [];
2536
+ });
2537
+ }
2538
+ if (typeof value === 'string') {
2539
+ return [`${field}: ${value}`];
2540
+ }
2541
+ if (isObject(value)) {
2542
+ const msg = value['message'] ?? value['msg'];
2543
+ if (typeof msg === 'string') {
2544
+ return [`${field}: ${msg}`];
2545
+ }
2546
+ }
2547
+ return [];
2548
+ });
2549
+ }
2550
+ function isObject(val) {
2551
+ return typeof val === 'object' && val !== null && !Array.isArray(val);
2552
+ }
2553
+ function extractMessageFromObject(value) {
2554
+ const msg = value['description'] ??
2555
+ value['message'] ??
2556
+ (isObject(value['error']) ? value['error']['message'] : value['error']) ??
2557
+ value['detail'] ??
2558
+ value['title'] ??
2559
+ value['reason'];
2560
+ if (typeof msg === 'string' && msg.trim().length > 0) {
2561
+ return msg.trim();
2562
+ }
2563
+ return null;
2564
+ }
2565
+ function truncate$1(value, max) {
2566
+ return value.length > max ? `${value.substring(0, max - 1)}…` : value;
2567
+ }
2568
+
2569
+ /** Set by error interceptor when it has already shown a toast for this error. */
2570
+ const TOAST_SHOWN_BY_INTERCEPTOR = '__toastShownByInterceptor';
2571
+ /**
2572
+ * Shows a toast for an API error.
2573
+ * Skips when error interceptor already showed a toast (avoids duplicates).
2574
+ * Use in subscribe error callbacks for all API calls.
2575
+ */
2576
+ function showApiErrorToast(toast, error) {
2577
+ if (error && typeof error === 'object' && error[TOAST_SHOWN_BY_INTERCEPTOR]) {
2578
+ return;
2579
+ }
2580
+ if (error instanceof HttpErrorResponse) {
2581
+ const message = extractApiErrorMessage(error);
2582
+ message ? toast.error(message) : toast.translateAndShow('error', HttpErrorKey.UNKNOWN);
2583
+ return;
2584
+ }
2585
+ const message = extractErrorMessage(error);
2586
+ if (message) {
2587
+ toast.error(message);
2588
+ }
2589
+ else {
2590
+ toast.translateAndShow('error', HttpErrorKey.UNKNOWN);
2591
+ }
2592
+ }
2593
+ /**
2594
+ * Creates an error handler for API subscribe callbacks.
2595
+ * Shows toast on error and optionally runs a callback (e.g. reset loading state).
2596
+ */
2597
+ function createApiErrorHandler(toast, onError) {
2598
+ return (error) => {
2599
+ showApiErrorToast(toast, error);
2600
+ onError?.();
2601
+ };
2602
+ }
2603
+ function extractErrorMessage(error) {
2604
+ if (error instanceof Error) {
2605
+ return error.message || null;
2606
+ }
2607
+ if (typeof error === 'string' && error.length > 0) {
2608
+ return error;
2609
+ }
2610
+ return null;
2611
+ }
2612
+
2613
+ const LOCALE_MAP = {
2614
+ ar: 'ar-SA',
2615
+ en: 'en-US',
2616
+ };
2617
+ function withArabicNumbering(lang, options) {
2618
+ if (lang !== 'ar') {
2619
+ return options;
2620
+ }
2621
+ return { ...options, numberingSystem: 'latn' };
2622
+ }
2623
+ const PRESET_FORMATS = {
2624
+ short: { day: 'numeric', month: 'numeric', year: 'numeric' },
2625
+ medium: { day: 'numeric', month: 'short', year: 'numeric' },
2626
+ long: { day: 'numeric', month: 'long', year: 'numeric' },
2627
+ full: { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' },
2628
+ time: { hour: '2-digit', minute: '2-digit' },
2629
+ datetime: { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' },
2630
+ dateTimeFull: {
2631
+ day: '2-digit',
2632
+ month: 'long',
2633
+ year: 'numeric',
2634
+ hour: 'numeric',
2635
+ minute: '2-digit',
2636
+ hour12: true,
2637
+ },
2638
+ /** Date + time design format: "18 يوليو 2024 - ص 9:22" (ar) / "18 July 2024 - 9:22 AM" (en) */
2639
+ recordDateTime: {
2640
+ day: 'numeric',
2641
+ month: 'long',
2642
+ year: 'numeric',
2643
+ hour: 'numeric',
2644
+ minute: '2-digit',
2645
+ hour12: true,
2646
+ },
2647
+ };
2648
+ /**
2649
+ * Formats a date according to the current app language.
2650
+ *
2651
+ * Usage:
2652
+ * {{ date | localizedDate }} → "12 أبريل 2025" (ar) / "12 April 2025" (en)
2653
+ * {{ date | localizedDate:'short' }} → "12/4/2025"
2654
+ * {{ date | localizedDate:'full' }} → "السبت، 12 أبريل 2025"
2655
+ * {{ date | localizedDate:'time' }} → "03:30 م"
2656
+ * {{ date | localizedDate:'datetime' }} → "12 أبريل 2025، 03:30 م"
2657
+ * {{ date | localizedDate:'dateTimeFull' }} → "12 أبريل 2025 3:30 م"
2658
+ * {{ date | localizedDate:'recordDateTime' }} → "18 يوليو 2024 - ص 9:22" (ar) / "18 July 2024 - 9:22 AM" (en)
2659
+ */
2660
+ class LocalizedDatePipe {
2661
+ translate = inject(TranslateService);
2662
+ transform(value, format = 'long') {
2663
+ if (value == null) {
2664
+ return '';
2665
+ }
2666
+ const date = toDate(value);
2667
+ if (!date) {
2668
+ return '';
2669
+ }
2670
+ const lang = this.translate.currentLang ?? I18N_CONFIG.DEFAULT_LANGUAGE;
2671
+ const locale = LOCALE_MAP[lang] ?? lang;
2672
+ if (format === 'recordDateTime') {
2673
+ const dateOptions = withArabicNumbering(lang, {
2674
+ day: 'numeric',
2675
+ month: 'long',
2676
+ year: 'numeric',
2677
+ });
2678
+ const dateStr = new Intl.DateTimeFormat(locale, dateOptions).format(date);
2679
+ const hour = date.getHours();
2680
+ const minute = date.getMinutes();
2681
+ const isPM = hour >= 12;
2682
+ const hour12 = hour % 12 || 12;
2683
+ const minPadded = minute.toString().padStart(2, '0');
2684
+ const timeStr = lang === 'ar' ? `${isPM ? 'م' : 'ص'} ${hour12}:${minPadded}` : `${hour12}:${minPadded} ${isPM ? 'PM' : 'AM'}`;
2685
+ return `${dateStr} - ${timeStr}`;
2686
+ }
2687
+ const options = withArabicNumbering(lang, PRESET_FORMATS[format] ?? PRESET_FORMATS['long']);
2688
+ return new Intl.DateTimeFormat(locale, options).format(date);
2689
+ }
2690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LocalizedDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2691
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: LocalizedDatePipe, isStandalone: true, name: "localizedDate", pure: false });
2692
+ }
2693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LocalizedDatePipe, decorators: [{
2694
+ type: Pipe,
2695
+ args: [{ name: 'localizedDate', pure: false }]
2696
+ }] });
2697
+ function toDate(value) {
2698
+ if (value instanceof Date) {
2699
+ return isNaN(value.getTime()) ? null : value;
2700
+ }
2701
+ const parsed = new Date(typeof value === 'string' ? value : Number(value));
2702
+ return isNaN(parsed.getTime()) ? null : parsed;
2703
+ }
2704
+
2705
+ const ARABIC_TO_ENGLISH = {
2706
+ '٠': '0',
2707
+ '١': '1',
2708
+ '٢': '2',
2709
+ '٣': '3',
2710
+ '٤': '4',
2711
+ '٥': '5',
2712
+ '٦': '6',
2713
+ '٧': '7',
2714
+ '٨': '8',
2715
+ '٩': '9',
2716
+ };
2717
+ /**
2718
+ * Converts numbers and strings to English numerals only (0–9).
2719
+ * Use when displaying counts, IDs, etc. in a bilingual context.
2720
+ *
2721
+ * Usage:
2722
+ * {{ count | englishNumber }} → "123" (always 0–9)
2723
+ * {{ "١٢٣" | englishNumber }} → "123"
2724
+ */
2725
+ class EnglishNumberPipe {
2726
+ transform(value) {
2727
+ if (value == null) {
2728
+ return '';
2729
+ }
2730
+ const str = String(value);
2731
+ return str.replace(/[٠-٩]/g, d => ARABIC_TO_ENGLISH[d] ?? d);
2732
+ }
2733
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EnglishNumberPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2734
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: EnglishNumberPipe, isStandalone: true, name: "englishNumber" });
2735
+ }
2736
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EnglishNumberPipe, decorators: [{
2737
+ type: Pipe,
2738
+ args: [{ name: 'englishNumber' }]
2739
+ }] });
2740
+
2741
+ /**
2742
+ * A pipe that replaces a substring within a string.
2743
+ *
2744
+ * Usage:
2745
+ * {{ value | replace: 'oldValue' : 'newValue' }}
2746
+ * {{ 'Hello World' | replace: 'World' : 'Angular' }} → 'Hello Angular'
2747
+ */
2748
+ class ReplacePipe {
2749
+ transform(value, searchValue, replaceValue = '') {
2750
+ if (!value) {
2751
+ return '';
2752
+ }
2753
+ if (!searchValue) {
2754
+ return value;
2755
+ }
2756
+ return value.replace(searchValue, replaceValue).trim();
2757
+ }
2758
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2759
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" });
2760
+ }
2761
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, decorators: [{
2762
+ type: Pipe,
2763
+ args: [{
2764
+ name: 'replace',
2765
+ standalone: true,
2766
+ }]
2767
+ }] });
2768
+
2769
+ const RTL_LANGUAGES = new Set([Language.AR]);
2770
+ class LanguageService {
2771
+ translate = inject(TranslateService);
2772
+ primeNG = inject(PrimeNG);
2773
+ doc = inject(DOCUMENT);
2774
+ destroyRef = inject(DestroyRef);
2775
+ platformId = inject(PLATFORM_ID);
2776
+ currentLang = signal(I18N_CONFIG.DEFAULT_LANGUAGE, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
2777
+ language = this.currentLang.asReadonly();
2778
+ isRtl = computed(() => RTL_LANGUAGES.has(this.currentLang()), ...(ngDevMode ? [{ debugName: "isRtl" }] : []));
2779
+ init() {
2780
+ const saved = this.getSavedLanguage();
2781
+ this.translate.setDefaultLang(I18N_CONFIG.DEFAULT_LANGUAGE);
2782
+ this.translate.use(saved);
2783
+ this.apply(saved);
2784
+ this.translate.onLangChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(({ lang }) => {
2785
+ this.persist(lang);
2786
+ this.apply(lang);
2787
+ });
2788
+ }
2789
+ setLanguage(lang) {
2790
+ this.translate.use(lang);
2791
+ }
2792
+ toggle() {
2793
+ const next = this.currentLang() === Language.AR ? Language.EN : Language.AR;
2794
+ this.setLanguage(next);
2795
+ }
2796
+ getSavedLanguage() {
2797
+ if (isPlatformBrowser(this.platformId)) {
2798
+ const stored = localStorage.getItem(STORAGE_KEYS.LANGUAGE);
2799
+ if (stored && Object.values(Language).includes(stored)) {
2800
+ return stored;
2801
+ }
2802
+ }
2803
+ this.persist(I18N_CONFIG.DEFAULT_LANGUAGE);
2804
+ return I18N_CONFIG.DEFAULT_LANGUAGE;
2805
+ }
2806
+ persist(lang) {
2807
+ if (isPlatformBrowser(this.platformId)) {
2808
+ localStorage.setItem(STORAGE_KEYS.LANGUAGE, lang);
2809
+ }
2810
+ }
2811
+ apply(lang) {
2812
+ this.currentLang.set(lang);
2813
+ const html = this.doc.documentElement;
2814
+ html.lang = lang;
2815
+ html.dir = RTL_LANGUAGES.has(lang) ? 'rtl' : 'ltr';
2816
+ const locale = PRIMENG_LOCALE[lang];
2817
+ if (locale) {
2818
+ this.primeNG.setTranslation(locale);
2819
+ }
2820
+ }
2821
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2822
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LanguageService, providedIn: 'root' });
2823
+ }
2824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LanguageService, decorators: [{
2825
+ type: Injectable,
2826
+ args: [{ providedIn: 'root' }]
2827
+ }] });
2828
+
2829
+ class ToastService {
2830
+ messageService = inject(MessageService);
2831
+ translate = inject(TranslateService);
2832
+ recentToasts = new Map();
2833
+ success(detail, summary, options) {
2834
+ this.show('success', detail, summary, options);
2835
+ }
2836
+ error(detail, summary, options) {
2837
+ this.show('error', detail, summary, { life: TOAST_DURATION.ERROR, ...options });
2838
+ }
2839
+ warn(detail, summary, options) {
2840
+ this.show('warn', detail, summary, options);
2841
+ }
2842
+ info(detail, summary, options) {
2843
+ this.show('info', detail, summary, options);
2844
+ }
2845
+ translateAndShow(severity, detailKey, summaryKey, interpolateParams, options) {
2846
+ const detail = this.translate.instant(detailKey, interpolateParams);
2847
+ const summary = summaryKey ? this.translate.instant(summaryKey, interpolateParams) : undefined;
2848
+ this.show(severity, detail, summary, options);
2849
+ }
2850
+ clear(key) {
2851
+ this.messageService.clear(key);
2852
+ }
2853
+ show(severity, detail, summary, options) {
2854
+ if (this.isDuplicate(`${severity}::${detail}`)) {
2855
+ return;
2856
+ }
2857
+ this.messageService.add({
2858
+ severity,
2859
+ summary: summary ?? this.resolveDefaultSummary(severity),
2860
+ detail,
2861
+ life: options?.life ?? TOAST_DURATION.DEFAULT,
2862
+ sticky: options?.sticky ?? false,
2863
+ closable: options?.closable ?? true,
2864
+ key: options?.key,
2865
+ });
2866
+ }
2867
+ isDuplicate(key) {
2868
+ const now = Date.now();
2869
+ const last = this.recentToasts.get(key);
2870
+ if (last && now - last < TOAST_DEDUP_MS) {
2871
+ return true;
2872
+ }
2873
+ this.recentToasts.set(key, now);
2874
+ this.pruneStaleEntries(now);
2875
+ return false;
2876
+ }
2877
+ pruneStaleEntries(now) {
2878
+ if (this.recentToasts.size <= TOAST_DEDUP_MAX_SIZE) {
2879
+ return;
2880
+ }
2881
+ for (const [k, ts] of this.recentToasts) {
2882
+ if (now - ts > TOAST_DEDUP_MS) {
2883
+ this.recentToasts.delete(k);
2884
+ }
2885
+ }
2886
+ }
2887
+ resolveDefaultSummary(severity) {
2888
+ const key = `TOAST.${severity.toUpperCase()}`;
2889
+ const translated = this.translate.instant(key);
2890
+ return translated !== key ? translated : '';
2891
+ }
2892
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2893
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ToastService, providedIn: 'root' });
2894
+ }
2895
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ToastService, decorators: [{
2896
+ type: Injectable,
2897
+ args: [{ providedIn: 'root' }]
2898
+ }] });
2899
+
2900
+ const SUPPRESSED_PATTERNS = ['ExpressionChangedAfterItHasBeenCheckedError', 'NG0100'];
2901
+ const CHUNK_LOAD_PATTERNS = ['Loading chunk', 'ChunkLoadError'];
2902
+ class GlobalErrorHandler {
2903
+ toast = null;
2904
+ env = inject(ENVIRONMENT);
2905
+ injector = inject(Injector);
2906
+ zone = inject(NgZone);
2907
+ handleError(error) {
2908
+ if (isAssetError(error)) {
2909
+ return;
2910
+ }
2911
+ if (extractHttpError(error)) {
2912
+ return;
2913
+ }
2914
+ const message = extractMessage(error);
2915
+ if (SUPPRESSED_PATTERNS.some(p => message.includes(p))) {
2916
+ return;
2917
+ }
2918
+ console.error('[GlobalErrorHandler]', error);
2919
+ this.runInZone(() => {
2920
+ const toast = this.resolveToast();
2921
+ if (!toast) {
2922
+ return;
2923
+ }
2924
+ if (CHUNK_LOAD_PATTERNS.some(p => message.includes(p))) {
2925
+ toast.translateAndShow('warn', HttpErrorKey.CHUNK_LOAD);
2926
+ return;
2927
+ }
2928
+ if (this.env.production) {
2929
+ toast.translateAndShow('error', HttpErrorKey.RUNTIME);
2930
+ }
2931
+ else {
2932
+ toast.error(truncate(message, ERROR_MESSAGE_MAX_LENGTH));
2933
+ }
2934
+ });
2935
+ }
2936
+ resolveToast() {
2937
+ if (!this.toast) {
2938
+ try {
2939
+ this.toast = this.injector.get(ToastService, null);
2940
+ }
2941
+ catch {
2942
+ return null;
2943
+ }
2944
+ }
2945
+ return this.toast;
2946
+ }
2947
+ runInZone(fn) {
2948
+ if (NgZone.isInAngularZone()) {
2949
+ fn();
2950
+ }
2951
+ else {
2952
+ this.zone.run(fn);
2953
+ }
2954
+ }
2955
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GlobalErrorHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2956
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GlobalErrorHandler });
2957
+ }
2958
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GlobalErrorHandler, decorators: [{
2959
+ type: Injectable
2960
+ }] });
2961
+ function extractMessage(error) {
2962
+ if (error instanceof Error) {
2963
+ return error.message;
2964
+ }
2965
+ if (typeof error === 'string') {
2966
+ return error;
2967
+ }
2968
+ try {
2969
+ return JSON.stringify(error);
2970
+ }
2971
+ catch {
2972
+ return 'Unknown error';
2973
+ }
2974
+ }
2975
+ function truncate(value, max) {
2976
+ return value.length > max ? `${value.substring(0, max - 1)}…` : value;
2977
+ }
2978
+ function isAssetError(error) {
2979
+ if (typeof error !== 'object' || error === null) {
2980
+ return false;
2981
+ }
2982
+ const url = error['url'];
2983
+ return typeof url === 'string' && ASSET_URL_PATTERN.test(url);
2984
+ }
2985
+ function extractHttpError(error) {
2986
+ if (error instanceof HttpErrorResponse) {
2987
+ return error;
2988
+ }
2989
+ if (typeof error !== 'object' || error === null) {
2990
+ return null;
2991
+ }
2992
+ const maybeRejection = error['rejection'];
2993
+ return maybeRejection instanceof HttpErrorResponse ? maybeRejection : null;
2994
+ }
2995
+
2996
+ const baseUrlInterceptor = (req, next) => {
2997
+ const { apiUrl } = inject(ENVIRONMENT);
2998
+ if (req.url.startsWith('http') ||
2999
+ req.url.startsWith('/') ||
3000
+ req.url.startsWith('assets/') ||
3001
+ req.url.startsWith('./assets/')) {
3002
+ return next(req);
3003
+ }
3004
+ const base = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
3005
+ const url = `${base}${req.url}`;
3006
+ return next(req.clone({ url }));
3007
+ };
3008
+
3009
+ let correlationCounter = 0;
3010
+ const authInterceptor = (req, next) => {
3011
+ if (req.context.get(SKIP_AUTH)) {
3012
+ return next(req);
3013
+ }
3014
+ const translate = inject(TranslateService);
3015
+ const auth = inject(AuthService);
3016
+ const lang = translate.currentLang ?? translate.defaultLang ?? FALLBACK_LANGUAGE;
3017
+ const isPublic = PUBLIC_API_PATHS.some(path => req.url.includes(path));
3018
+ let headers = req.headers
3019
+ .set(HTTP_HEADERS.ACCEPT_LANGUAGE, lang)
3020
+ .set(HTTP_HEADERS.CORRELATION_ID, generateCorrelationId());
3021
+ if (!isPublic) {
3022
+ const token = auth.getAccessToken();
3023
+ if (token) {
3024
+ headers = headers.set(HTTP_HEADERS.AUTHORIZATION, `Bearer ${token}`);
3025
+ }
3026
+ }
3027
+ return next(req.clone({ headers }));
3028
+ };
3029
+ function generateCorrelationId() {
3030
+ const timestamp = Date.now().toString(36);
3031
+ const counter = (++correlationCounter).toString(36);
3032
+ const random = Math.random().toString(36).substring(2, 6);
3033
+ return `${timestamp}-${counter}-${random}`;
3034
+ }
3035
+
3036
+ const cacheInterceptor = (req, next) => {
3037
+ const cache = inject(HttpCacheService);
3038
+ if (req.method !== 'GET') {
3039
+ return next(req).pipe(tap(event => {
3040
+ if (event instanceof HttpResponse && event.ok) {
3041
+ cache.clearPattern(new RegExp(escapeRegex(extractBasePath(req.url))));
3042
+ }
3043
+ }));
3044
+ }
3045
+ if (!req.context.get(CACHE_ENABLED)) {
3046
+ return next(req);
3047
+ }
3048
+ const ttl = req.context.get(CACHE_TTL);
3049
+ const cached = cache.get(req.urlWithParams, ttl);
3050
+ if (cached) {
3051
+ return of(cached.clone());
3052
+ }
3053
+ return next(req).pipe(tap(event => {
3054
+ if (event instanceof HttpResponse && event.ok) {
3055
+ cache.set(req.urlWithParams, event);
3056
+ }
3057
+ }), share());
3058
+ };
3059
+ function extractBasePath(url) {
3060
+ try {
3061
+ const { pathname } = new URL(url, 'https://placeholder');
3062
+ const segments = pathname.split('/').filter(Boolean);
3063
+ return `/${segments.slice(0, Math.min(segments.length, CACHE_PATH_DEPTH)).join('/')}`;
3064
+ }
3065
+ catch {
3066
+ return url.split('?')[0];
3067
+ }
3068
+ }
3069
+ function escapeRegex(str) {
3070
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3071
+ }
3072
+
3073
+ let lastLogoutTimestamp = 0;
3074
+ const STATUS_TO_KEY = new Map([
3075
+ [HttpStatusCode.PaymentRequired, { key: HttpErrorKey.PAYMENT_REQUIRED, severity: 'warn' }],
3076
+ [HttpStatusCode.Forbidden, { key: HttpErrorKey.FORBIDDEN, severity: 'error' }],
3077
+ [HttpStatusCode.MethodNotAllowed, { key: HttpErrorKey.METHOD_NOT_ALLOWED, severity: 'error' }],
3078
+ [HttpStatusCode.NotAcceptable, { key: HttpErrorKey.NOT_ACCEPTABLE, severity: 'error' }],
3079
+ [HttpStatusCode.RequestTimeout, { key: HttpErrorKey.TIMEOUT, severity: 'error' }],
3080
+ [HttpStatusCode.Gone, { key: HttpErrorKey.GONE, severity: 'warn' }],
3081
+ [HttpStatusCode.PayloadTooLarge, { key: HttpErrorKey.PAYLOAD_TOO_LARGE, severity: 'error' }],
3082
+ [HttpStatusCode.UriTooLong, { key: HttpErrorKey.URI_TOO_LONG, severity: 'error' }],
3083
+ [HttpStatusCode.UnsupportedMediaType, { key: HttpErrorKey.UNSUPPORTED_MEDIA_TYPE, severity: 'error' }],
3084
+ [HttpStatusCode.Locked, { key: HttpErrorKey.LOCKED, severity: 'warn' }],
3085
+ [HttpStatusCode.NotImplemented, { key: HttpErrorKey.NOT_IMPLEMENTED, severity: 'error' }],
3086
+ [HttpStatusCode.BadGateway, { key: HttpErrorKey.BAD_GATEWAY, severity: 'error' }],
3087
+ [HttpStatusCode.GatewayTimeout, { key: HttpErrorKey.GATEWAY_TIMEOUT, severity: 'error' }],
3088
+ ]);
3089
+ const errorInterceptor = (req, next) => {
3090
+ if (req.context.get(SKIP_ERROR_HANDLING)) {
3091
+ return next(req);
3092
+ }
3093
+ const router = inject(Router);
3094
+ const toast = inject(ToastService);
3095
+ const auth = inject(AuthService);
3096
+ return next(req).pipe(catchError((error) => {
3097
+ const url = error instanceof HttpErrorResponse ? (error.url ?? req.url) : req.url;
3098
+ if (isAssetRequest(url)) {
3099
+ return throwError(() => error);
3100
+ }
3101
+ if (error instanceof TimeoutError) {
3102
+ toast.translateAndShow('error', HttpErrorKey.TIMEOUT);
3103
+ error[TOAST_SHOWN_BY_INTERCEPTOR] = true;
3104
+ return throwError(() => error);
3105
+ }
3106
+ if (!(error instanceof HttpErrorResponse)) {
3107
+ toast.translateAndShow('error', HttpErrorKey.UNKNOWN);
3108
+ error[TOAST_SHOWN_BY_INTERCEPTOR] = true;
3109
+ return throwError(() => error);
3110
+ }
3111
+ if (error.status === 0) {
3112
+ handleNetworkError(error, toast);
3113
+ }
3114
+ else {
3115
+ handleHttpStatus(error, toast, auth, router);
3116
+ }
3117
+ error[TOAST_SHOWN_BY_INTERCEPTOR] = true;
3118
+ return throwError(() => error);
3119
+ }));
3120
+ };
3121
+ function handleNetworkError(error, toast) {
3122
+ if (error.url && error.statusText === 'Unknown Error') {
3123
+ toast.translateAndShow('error', HttpErrorKey.CORS);
3124
+ }
3125
+ else if (!navigator.onLine) {
3126
+ toast.translateAndShow('error', HttpErrorKey.OFFLINE);
3127
+ }
3128
+ else {
3129
+ toast.translateAndShow('error', HttpErrorKey.NETWORK);
3130
+ }
3131
+ }
3132
+ function handleHttpStatus(error, toast, auth, router) {
3133
+ const simple = STATUS_TO_KEY.get(error.status);
3134
+ if (simple) {
3135
+ toast.translateAndShow(simple.severity, simple.key);
3136
+ return;
3137
+ }
3138
+ switch (error.status) {
3139
+ case HttpStatusCode.NotFound:
3140
+ showApiError(error, toast, HttpErrorKey.NOT_FOUND);
3141
+ break;
3142
+ case HttpStatusCode.BadRequest:
3143
+ case HttpStatusCode.Conflict:
3144
+ showApiError(error, toast, error.status === HttpStatusCode.BadRequest ? HttpErrorKey.BAD_REQUEST : HttpErrorKey.CONFLICT);
3145
+ break;
3146
+ case HttpStatusCode.Unauthorized:
3147
+ handleUnauthorized(auth, router, toast);
3148
+ break;
3149
+ case HttpStatusCode.UnprocessableEntity:
3150
+ showFieldErrors(error, toast);
3151
+ break;
3152
+ case HttpStatusCode.TooManyRequests:
3153
+ handleRateLimit(error, toast);
3154
+ break;
3155
+ case HttpStatusCode.InternalServerError:
3156
+ showApiError(error, toast, HttpErrorKey.SERVER);
3157
+ break;
3158
+ case HttpStatusCode.ServiceUnavailable:
3159
+ handleServiceUnavailable(error, toast);
3160
+ break;
3161
+ default:
3162
+ showApiError(error, toast, error.status >= 500 ? HttpErrorKey.SERVER : HttpErrorKey.UNKNOWN);
3163
+ }
3164
+ }
3165
+ function handleUnauthorized(auth, router, toast) {
3166
+ const now = Date.now();
3167
+ if (now - lastLogoutTimestamp < LOGOUT_DEBOUNCE_MS) {
3168
+ return;
3169
+ }
3170
+ lastLogoutTimestamp = now;
3171
+ auth.logout();
3172
+ toast.translateAndShow('warn', HttpErrorKey.SESSION_EXPIRED);
3173
+ void router.navigate([ROUTE_PATHS.AUTH_LOGIN]);
3174
+ }
3175
+ function handleRateLimit(error, toast) {
3176
+ const retryAfter = error.headers?.get(HTTP_HEADERS.RETRY_AFTER);
3177
+ if (retryAfter) {
3178
+ const seconds = parseInt(retryAfter, 10);
3179
+ if (!isNaN(seconds)) {
3180
+ toast.translateAndShow('warn', HttpErrorKey.RATE_LIMIT_RETRY, undefined, { seconds: String(seconds) });
3181
+ return;
3182
+ }
3183
+ }
3184
+ toast.translateAndShow('warn', HttpErrorKey.TOO_MANY_REQUESTS);
3185
+ }
3186
+ function handleServiceUnavailable(error, toast) {
3187
+ const isMaintenance = error.headers?.get(HTTP_HEADERS.MAINTENANCE) === 'true';
3188
+ toast.translateAndShow(isMaintenance ? 'info' : 'error', isMaintenance ? HttpErrorKey.MAINTENANCE : HttpErrorKey.SERVICE_UNAVAILABLE);
3189
+ }
3190
+ function showApiError(error, toast, fallbackKey) {
3191
+ const message = extractApiErrorMessage(error);
3192
+ if (message) {
3193
+ toast.error(message);
3194
+ }
3195
+ else {
3196
+ toast.translateAndShow('error', fallbackKey);
3197
+ }
3198
+ }
3199
+ function showFieldErrors(error, toast) {
3200
+ const fieldErrors = extractFieldErrors(error);
3201
+ if (fieldErrors) {
3202
+ toast.error(fieldErrors);
3203
+ }
3204
+ else {
3205
+ showApiError(error, toast, HttpErrorKey.VALIDATION);
3206
+ }
3207
+ }
3208
+ function isAssetRequest(url) {
3209
+ return !!url && typeof url === 'string' && ASSET_URL_PATTERN.test(url);
3210
+ }
3211
+
3212
+ /**
3213
+ * Tracks browser online/offline state via the native browser events.
3214
+ * Exposes `isOnline` as a signal for reactive UI (e.g. offline banner).
3215
+ * Shows a toast when the connection changes.
3216
+ *
3217
+ * Eagerly initialised via APP_INITIALIZER in app.config.ts so event
3218
+ * listeners start before the first HTTP request.
3219
+ */
3220
+ class ConnectivityService {
3221
+ toast = inject(ToastService);
3222
+ destroyRef = inject(DestroyRef);
3223
+ /** Reactive online state — true when navigator.onLine is true. */
3224
+ isOnline = signal(navigator.onLine, ...(ngDevMode ? [{ debugName: "isOnline" }] : []));
3225
+ constructor() {
3226
+ fromEvent(window, 'online')
3227
+ .pipe(takeUntilDestroyed(this.destroyRef))
3228
+ .subscribe(() => {
3229
+ this.isOnline.set(true);
3230
+ this.toast.translateAndShow('success', HttpErrorKey.BACK_ONLINE);
3231
+ });
3232
+ fromEvent(window, 'offline')
3233
+ .pipe(takeUntilDestroyed(this.destroyRef))
3234
+ .subscribe(() => {
3235
+ this.isOnline.set(false);
3236
+ this.toast.translateAndShow('error', HttpErrorKey.OFFLINE);
3237
+ });
3238
+ }
3239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConnectivityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3240
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConnectivityService, providedIn: 'root' });
3241
+ }
3242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConnectivityService, decorators: [{
3243
+ type: Injectable,
3244
+ args: [{ providedIn: 'root' }]
3245
+ }], ctorParameters: () => [] });
3246
+
3247
+ /** Set to true to skip offline check for a specific request (e.g. ping/health-check). */
3248
+ const SKIP_CONNECTIVITY = new HttpContextToken(() => false);
3249
+ /**
3250
+ * Short-circuits outgoing HTTP requests when the browser is offline.
3251
+ * Throws an HttpErrorResponse with status 0 so the existing errorInterceptor
3252
+ * picks it up and shows the OFFLINE toast — without the browser queuing the
3253
+ * request and waiting for a timeout.
3254
+ *
3255
+ * Skips asset requests (i18n JSON, SVG icons, etc.) so the app shell can
3256
+ * still load even when the service worker serves those from cache.
3257
+ */
3258
+ const connectivityInterceptor = (req, next) => {
3259
+ if (req.context.get(SKIP_CONNECTIVITY) || ASSET_URL_PATTERN.test(req.url)) {
3260
+ return next(req);
3261
+ }
3262
+ const connectivity = inject(ConnectivityService);
3263
+ if (!connectivity.isOnline()) {
3264
+ return throwError(() => new HttpErrorResponse({
3265
+ error: new ProgressEvent('error'),
3266
+ status: 0,
3267
+ statusText: 'Offline',
3268
+ url: req.url,
3269
+ }));
3270
+ }
3271
+ return next(req);
3272
+ };
3273
+
3274
+ /** Redirects unauthenticated users to /auth/login. */
3275
+ const authGuard = () => {
3276
+ const authService = inject(AuthService);
3277
+ const router = inject(Router);
3278
+ if (authService.isAuthenticated()) {
3279
+ return true;
3280
+ }
3281
+ return router.createUrlTree([ROUTE_PATHS.AUTH_LOGIN]);
3282
+ };
3283
+ /** Redirects authenticated users away from auth/register pages. */
3284
+ const guestGuard = () => {
3285
+ const authService = inject(AuthService);
3286
+ const router = inject(Router);
3287
+ if (!authService.isAuthenticated()) {
3288
+ return true;
3289
+ }
3290
+ return router.createUrlTree([ROUTE_PATHS.DASHBOARD]);
3291
+ };
3292
+
3293
+ /** Allows access only when the user has all permissions listed in route data. */
3294
+ const permissionGuard = (route) => {
3295
+ const authService = inject(AuthService);
3296
+ const router = inject(Router);
3297
+ const requiredPermissions = route.data.permissions;
3298
+ if (!requiredPermissions || requiredPermissions.length === 0) {
3299
+ return true;
3300
+ }
3301
+ const hasAll = requiredPermissions.every(p => authService.hasPermission(p));
3302
+ if (hasAll) {
3303
+ return true;
3304
+ }
3305
+ // Redirect to unauthorized page or dashboard
3306
+ // For now, consistent behavior: redirect to home/dashboard if unauthorized
3307
+ return router.createUrlTree([ROUTE_PATHS.DASHBOARD]);
3308
+ };
3309
+
3310
+ class ArabicOnlyDirective {
3311
+ el = inject(ElementRef);
3312
+ onInputChange(event) {
3313
+ const input = this.el.nativeElement;
3314
+ const initialValue = input.value;
3315
+ const newValue = initialValue.replace(REGEX.NON_ARABIC, '');
3316
+ if (initialValue !== newValue) {
3317
+ input.value = newValue;
3318
+ // Propagate change event if using ReactiveForms
3319
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3320
+ }
3321
+ }
3322
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ArabicOnlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3323
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ArabicOnlyDirective, isStandalone: true, selector: "[ncimArabicOnly]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3324
+ }
3325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ArabicOnlyDirective, decorators: [{
3326
+ type: Directive,
3327
+ args: [{
3328
+ selector: '[ncimArabicOnly]',
3329
+ }]
3330
+ }], propDecorators: { onInputChange: [{
3331
+ type: HostListener,
3332
+ args: ['input', ['$event']]
3333
+ }] } });
3334
+
3335
+ class EnglishOnlyDirective {
3336
+ el = inject(ElementRef);
3337
+ onInputChange(event) {
3338
+ const input = this.el.nativeElement;
3339
+ const initialValue = input.value;
3340
+ const newValue = initialValue.replace(REGEX.NON_ENGLISH, '');
3341
+ if (initialValue !== newValue) {
3342
+ input.value = newValue;
3343
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3344
+ }
3345
+ }
3346
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EnglishOnlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3347
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: EnglishOnlyDirective, isStandalone: true, selector: "[ncimEnglishOnly]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3348
+ }
3349
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EnglishOnlyDirective, decorators: [{
3350
+ type: Directive,
3351
+ args: [{
3352
+ selector: '[ncimEnglishOnly]',
3353
+ }]
3354
+ }], propDecorators: { onInputChange: [{
3355
+ type: HostListener,
3356
+ args: ['input', ['$event']]
3357
+ }] } });
3358
+
3359
+ class NumbersOnlyDirective {
3360
+ el = inject(ElementRef);
3361
+ onInputChange(event) {
3362
+ const input = this.el.nativeElement;
3363
+ const initialValue = input.value;
3364
+ const newValue = initialValue.replace(REGEX.NON_NUMBERS, '');
3365
+ if (initialValue !== newValue) {
3366
+ input.value = newValue;
3367
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3368
+ }
3369
+ }
3370
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NumbersOnlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3371
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: NumbersOnlyDirective, isStandalone: true, selector: "[ncimNumbersOnly]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3372
+ }
3373
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NumbersOnlyDirective, decorators: [{
3374
+ type: Directive,
3375
+ args: [{
3376
+ selector: '[ncimNumbersOnly]',
3377
+ }]
3378
+ }], propDecorators: { onInputChange: [{
3379
+ type: HostListener,
3380
+ args: ['input', ['$event']]
3381
+ }] } });
3382
+
3383
+ class PhoneOnlyDirective {
3384
+ el = inject(ElementRef);
3385
+ onInputChange() {
3386
+ const input = this.el.nativeElement;
3387
+ const initialValue = input.value;
3388
+ const cursorPos = input.selectionStart ?? initialValue.length;
3389
+ const digitsOnly = initialValue.replace(REGEX.NON_DIGITS, '');
3390
+ const newValue = digitsOnly.slice(0, VALIDATION_LENGTHS.MOBILE);
3391
+ if (initialValue !== newValue) {
3392
+ const nextCursor = Math.min(cursorPos, newValue.length);
3393
+ input.value = newValue;
3394
+ input.setSelectionRange(nextCursor, nextCursor);
3395
+ }
3396
+ }
3397
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PhoneOnlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3398
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: PhoneOnlyDirective, isStandalone: true, selector: "input[ncimPhoneOnly]", host: { listeners: { "input": "onInputChange()" } }, ngImport: i0 });
3399
+ }
3400
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PhoneOnlyDirective, decorators: [{
3401
+ type: Directive,
3402
+ args: [{
3403
+ selector: 'input[ncimPhoneOnly]',
3404
+ }]
3405
+ }], propDecorators: { onInputChange: [{
3406
+ type: HostListener,
3407
+ args: ['input']
3408
+ }] } });
3409
+
3410
+ class AlphaNumericDirective {
3411
+ el = inject(ElementRef);
3412
+ onInputChange(event) {
3413
+ const input = this.el.nativeElement;
3414
+ const initialValue = input.value;
3415
+ const newValue = initialValue.replace(REGEX.NON_ALPHANUMERIC, '');
3416
+ if (initialValue !== newValue) {
3417
+ input.value = newValue;
3418
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3419
+ }
3420
+ }
3421
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AlphaNumericDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3422
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: AlphaNumericDirective, isStandalone: true, selector: "[ncimAlphaNumeric]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3423
+ }
3424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AlphaNumericDirective, decorators: [{
3425
+ type: Directive,
3426
+ args: [{
3427
+ selector: '[ncimAlphaNumeric]',
3428
+ }]
3429
+ }], propDecorators: { onInputChange: [{
3430
+ type: HostListener,
3431
+ args: ['input', ['$event']]
3432
+ }] } });
3433
+
3434
+ /** Trims whitespace on blur. */
3435
+ class TrimInputDirective {
3436
+ el = inject(ElementRef);
3437
+ ngControl = inject(NgControl, { optional: true });
3438
+ onBlur() {
3439
+ const input = this.el.nativeElement;
3440
+ const trimmed = input.value.trim();
3441
+ if (input.value !== trimmed) {
3442
+ input.value = trimmed;
3443
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3444
+ // If using ReactiveForms, update the control explicitly
3445
+ if (this.ngControl && this.ngControl.control) {
3446
+ this.ngControl.control.setValue(trimmed);
3447
+ }
3448
+ }
3449
+ }
3450
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TrimInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3451
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: TrimInputDirective, isStandalone: true, selector: "[ncimTrimInput]", host: { listeners: { "blur": "onBlur()" } }, ngImport: i0 });
3452
+ }
3453
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TrimInputDirective, decorators: [{
3454
+ type: Directive,
3455
+ args: [{
3456
+ selector: '[ncimTrimInput]',
3457
+ }]
3458
+ }], propDecorators: { onBlur: [{
3459
+ type: HostListener,
3460
+ args: ['blur']
3461
+ }] } });
3462
+
3463
+ /** Prevents paste on the host element. */
3464
+ class PreventPasteDirective {
3465
+ onPaste(event) {
3466
+ event.preventDefault();
3467
+ }
3468
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PreventPasteDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3469
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: PreventPasteDirective, isStandalone: true, selector: "[ncimPreventPaste]", host: { listeners: { "paste": "onPaste($event)" } }, ngImport: i0 });
3470
+ }
3471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PreventPasteDirective, decorators: [{
3472
+ type: Directive,
3473
+ args: [{
3474
+ selector: '[ncimPreventPaste]',
3475
+ }]
3476
+ }], propDecorators: { onPaste: [{
3477
+ type: HostListener,
3478
+ args: ['paste', ['$event']]
3479
+ }] } });
3480
+
3481
+ class ToUppercaseDirective {
3482
+ el = inject(ElementRef);
3483
+ ngControl = inject(NgControl, { optional: true });
3484
+ onInput(event) {
3485
+ const input = this.el.nativeElement;
3486
+ const start = input.selectionStart;
3487
+ const end = input.selectionEnd;
3488
+ const upper = input.value.toUpperCase();
3489
+ if (input.value !== upper) {
3490
+ input.value = upper;
3491
+ input.setSelectionRange(start, end);
3492
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3493
+ if (this.ngControl && this.ngControl.control) {
3494
+ this.ngControl.control.setValue(upper, { emitEvent: false });
3495
+ }
3496
+ }
3497
+ }
3498
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ToUppercaseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3499
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ToUppercaseDirective, isStandalone: true, selector: "[ncimToUppercase]", host: { listeners: { "input": "onInput($event)" } }, ngImport: i0 });
3500
+ }
3501
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ToUppercaseDirective, decorators: [{
3502
+ type: Directive,
3503
+ args: [{
3504
+ selector: '[ncimToUppercase]',
3505
+ }]
3506
+ }], propDecorators: { onInput: [{
3507
+ type: HostListener,
3508
+ args: ['input', ['$event']]
3509
+ }] } });
3510
+
3511
+ /** Focuses the host element after the first render. */
3512
+ class AutofocusDirective {
3513
+ el = inject((ElementRef));
3514
+ constructor() {
3515
+ afterNextRender(() => this.el.nativeElement.focus());
3516
+ }
3517
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AutofocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3518
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: AutofocusDirective, isStandalone: true, selector: "[ncimAutofocus]", ngImport: i0 });
3519
+ }
3520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AutofocusDirective, decorators: [{
3521
+ type: Directive,
3522
+ args: [{
3523
+ selector: '[ncimAutofocus]',
3524
+ }]
3525
+ }], ctorParameters: () => [] });
3526
+
3527
+ /** Selects all text on click/focus. */
3528
+ class SelectOnClickDirective {
3529
+ el = inject(ElementRef);
3530
+ onClick() {
3531
+ const input = this.el.nativeElement;
3532
+ input.select();
3533
+ }
3534
+ onFocus() {
3535
+ const input = this.el.nativeElement;
3536
+ input.select();
3537
+ }
3538
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SelectOnClickDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3539
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: SelectOnClickDirective, isStandalone: true, selector: "[ncimSelectOnClick]", host: { listeners: { "click": "onClick()", "focus": "onFocus()" } }, ngImport: i0 });
3540
+ }
3541
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SelectOnClickDirective, decorators: [{
3542
+ type: Directive,
3543
+ args: [{
3544
+ selector: '[ncimSelectOnClick]',
3545
+ }]
3546
+ }], propDecorators: { onClick: [{
3547
+ type: HostListener,
3548
+ args: ['click']
3549
+ }], onFocus: [{
3550
+ type: HostListener,
3551
+ args: ['focus']
3552
+ }] } });
3553
+
3554
+ class NoWhitespaceDirective {
3555
+ el = inject(ElementRef);
3556
+ onInputChange(event) {
3557
+ const input = this.el.nativeElement;
3558
+ const initialValue = input.value;
3559
+ const newValue = initialValue.replace(REGEX.WHITESPACE_GLOBAL, '');
3560
+ if (initialValue !== newValue) {
3561
+ input.value = newValue;
3562
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3563
+ }
3564
+ }
3565
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NoWhitespaceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3566
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: NoWhitespaceDirective, isStandalone: true, selector: "[ncimNoWhitespace]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3567
+ }
3568
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NoWhitespaceDirective, decorators: [{
3569
+ type: Directive,
3570
+ args: [{
3571
+ selector: '[ncimNoWhitespace]',
3572
+ }]
3573
+ }], propDecorators: { onInputChange: [{
3574
+ type: HostListener,
3575
+ args: ['input', ['$event']]
3576
+ }] } });
3577
+
3578
+ class EmailCharsDirective {
3579
+ el = inject(ElementRef);
3580
+ onInputChange(event) {
3581
+ const input = this.el.nativeElement;
3582
+ const initialValue = input.value;
3583
+ const newValue = initialValue.replace(REGEX.NON_EMAIL_CHARS, '');
3584
+ if (initialValue !== newValue) {
3585
+ input.value = newValue;
3586
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3587
+ }
3588
+ }
3589
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailCharsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3590
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: EmailCharsDirective, isStandalone: true, selector: "[ncimEmailChars]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 });
3591
+ }
3592
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailCharsDirective, decorators: [{
3593
+ type: Directive,
3594
+ args: [{
3595
+ selector: '[ncimEmailChars]',
3596
+ }]
3597
+ }], propDecorators: { onInputChange: [{
3598
+ type: HostListener,
3599
+ args: ['input', ['$event']]
3600
+ }] } });
3601
+
3602
+ /** Shows content only when user has all required permissions. */
3603
+ class HasPermissionDirective {
3604
+ templateRef = inject(TemplateRef);
3605
+ viewContainer = inject(ViewContainerRef);
3606
+ authService = inject(AuthService);
3607
+ permissions = input([], { ...(ngDevMode ? { debugName: "permissions" } : {}), alias: 'ncimHasPermission' });
3608
+ requiredPermissions = computed(() => {
3609
+ const p = this.permissions();
3610
+ return Array.isArray(p) ? p : [p];
3611
+ }, ...(ngDevMode ? [{ debugName: "requiredPermissions" }] : []));
3612
+ constructor() {
3613
+ effect(() => {
3614
+ this.authService.isAuthenticated();
3615
+ this.requiredPermissions();
3616
+ this.updateView();
3617
+ });
3618
+ }
3619
+ updateView() {
3620
+ // Now AuthService handles array logic (checking ALL)
3621
+ const hasPermission = this.authService.hasPermission(this.requiredPermissions());
3622
+ if (hasPermission) {
3623
+ if (this.viewContainer.length === 0) {
3624
+ this.viewContainer.createEmbeddedView(this.templateRef);
3625
+ }
3626
+ }
3627
+ else {
3628
+ this.viewContainer.clear();
3629
+ }
3630
+ }
3631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HasPermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3632
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: HasPermissionDirective, isStandalone: true, selector: "[ncimHasPermission]", inputs: { permissions: { classPropertyName: "permissions", publicName: "ncimHasPermission", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
3633
+ }
3634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HasPermissionDirective, decorators: [{
3635
+ type: Directive,
3636
+ args: [{
3637
+ selector: '[ncimHasPermission]',
3638
+ }]
3639
+ }], ctorParameters: () => [], propDecorators: { permissions: [{ type: i0.Input, args: [{ isSignal: true, alias: "ncimHasPermission", required: false }] }] } });
3640
+
3641
+ class ImgFallbackDirective {
3642
+ translateService = inject(TranslateService);
3643
+ el = inject(ElementRef);
3644
+ renderer = inject(Renderer2);
3645
+ fallbackUrl = input(undefined, { ...(ngDevMode ? { debugName: "fallbackUrl" } : {}), alias: 'ncimImgFallback' });
3646
+ fallbackTitle = input(undefined, ...(ngDevMode ? [{ debugName: "fallbackTitle" }] : []));
3647
+ onError() {
3648
+ const fallback = this.fallbackUrl() || 'assets/images/image-not-found-placeholder.svg';
3649
+ const defaultTitle = this.translateService.instant('COMMON.IMAGE_NOT_FOUND');
3650
+ this.renderer.setAttribute(this.el.nativeElement, 'src', fallback);
3651
+ this.renderer.setAttribute(this.el.nativeElement, 'title', this.fallbackTitle() || defaultTitle);
3652
+ }
3653
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ImgFallbackDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3654
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: ImgFallbackDirective, isStandalone: true, selector: "img[ncimImgFallback]", inputs: { fallbackUrl: { classPropertyName: "fallbackUrl", publicName: "ncimImgFallback", isSignal: true, isRequired: false, transformFunction: null }, fallbackTitle: { classPropertyName: "fallbackTitle", publicName: "fallbackTitle", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "error": "onError()" } }, ngImport: i0 });
3655
+ }
3656
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ImgFallbackDirective, decorators: [{
3657
+ type: Directive,
3658
+ args: [{
3659
+ selector: 'img[ncimImgFallback]',
3660
+ standalone: true,
3661
+ host: {
3662
+ '(error)': 'onError()',
3663
+ },
3664
+ }]
3665
+ }], propDecorators: { fallbackUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "ncimImgFallback", required: false }] }], fallbackTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackTitle", required: false }] }] } });
3666
+
3667
+ class PluralTranslateCompiler extends TranslateCompiler {
3668
+ pluralRules = new Intl.PluralRules('ar');
3669
+ compile(value, lang) {
3670
+ if (this.isPlural(value)) {
3671
+ return this.compilePlural(value, lang);
3672
+ }
3673
+ return value;
3674
+ }
3675
+ compileTranslations(translations, lang) {
3676
+ const result = {};
3677
+ for (const key in translations) {
3678
+ const value = translations[key];
3679
+ if (typeof value === 'string') {
3680
+ result[key] = this.compile(value, lang);
3681
+ }
3682
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
3683
+ result[key] = this.compileTranslations(value, lang);
3684
+ }
3685
+ }
3686
+ return result;
3687
+ }
3688
+ isPlural(value) {
3689
+ return value.includes('|');
3690
+ }
3691
+ compilePlural(value, lang) {
3692
+ const parts = value.split('|').map(p => p.trim());
3693
+ const rules = new Intl.PluralRules(lang);
3694
+ return (params) => {
3695
+ let count = params?.count ?? 0;
3696
+ // Handle cases where count might be an Arabic string number
3697
+ if (typeof count === 'string') {
3698
+ const arabicToEnglish = {
3699
+ '٠': '0',
3700
+ '١': '1',
3701
+ '٢': '2',
3702
+ '٣': '3',
3703
+ '٤': '4',
3704
+ '٥': '5',
3705
+ '٦': '6',
3706
+ '٧': '7',
3707
+ '٨': '8',
3708
+ '٩': '9',
3709
+ };
3710
+ count = count.replace(/[٠-٩]/g, d => arabicToEnglish[d] ?? d);
3711
+ count = parseFloat(count);
3712
+ }
3713
+ const rule = rules.select(count);
3714
+ // Intl.PluralRules for 'ar' returns: 'zero', 'one', 'two', 'few', 'many', 'other'
3715
+ // We expect the string to have parts in this order:
3716
+ // zero | one | two | few | many | other
3717
+ let index;
3718
+ switch (rule) {
3719
+ case 'zero':
3720
+ index = 0;
3721
+ break;
3722
+ case 'one':
3723
+ index = 1;
3724
+ break;
3725
+ case 'two':
3726
+ index = 2;
3727
+ break;
3728
+ case 'few':
3729
+ index = 3;
3730
+ break;
3731
+ case 'many':
3732
+ index = 4;
3733
+ break;
3734
+ case 'other':
3735
+ index = 5;
3736
+ break;
3737
+ default:
3738
+ index = 5;
3739
+ }
3740
+ // If the specific part is missing, fallback to 'other' (last part)
3741
+ const result = parts[index] ?? parts[parts.length - 1];
3742
+ return result.replace(/\{\{\s*count\s*\}\}/g, params?.count?.toString() ?? '0');
3743
+ };
3744
+ }
3745
+ }
3746
+
3747
+ const NcimPreset = definePreset(Aura, {
3748
+ primitive: {
3749
+ borderRadius: {
3750
+ none: '0',
3751
+ xs: '2px',
3752
+ sm: '4px',
3753
+ md: '6px',
3754
+ lg: '8px',
3755
+ xl: '12px',
3756
+ },
3757
+ ncim: {
3758
+ 50: '#f0fdf4',
3759
+ 100: '#dcfce7',
3760
+ 200: '#bbf7d0',
3761
+ 300: '#6ee7b7',
3762
+ 400: '#34d399',
3763
+ 500: '#1b8354',
3764
+ 600: '#1a8a3e',
3765
+ 700: '#15722f',
3766
+ 800: '#166534',
3767
+ 900: '#14532d',
3768
+ 950: '#052e16',
3769
+ },
3770
+ },
3771
+ semantic: {
3772
+ primary: {
3773
+ 50: '{ncim.50}',
3774
+ 100: '{ncim.100}',
3775
+ 200: '{ncim.200}',
3776
+ 300: '{ncim.300}',
3777
+ 400: '{ncim.400}',
3778
+ 500: '{ncim.500}',
3779
+ 600: '{ncim.600}',
3780
+ 700: '{ncim.700}',
3781
+ 800: '{ncim.800}',
3782
+ 900: '{ncim.900}',
3783
+ 950: '{ncim.950}',
3784
+ },
3785
+ transitionDuration: '0.2s',
3786
+ formField: {
3787
+ borderRadius: '{border.radius.md}',
3788
+ focusRing: {
3789
+ width: '1px',
3790
+ style: 'solid',
3791
+ color: '{primary.color}',
3792
+ offset: '0',
3793
+ shadow: 'none',
3794
+ },
3795
+ },
3796
+ colorScheme: {
3797
+ light: {
3798
+ surface: {
3799
+ 0: '#ffffff',
3800
+ 50: '#f8fafc',
3801
+ 100: '#f3f4f6',
3802
+ 200: '#e5e7eb',
3803
+ 300: '#d2d6db',
3804
+ 400: '#9da4ae',
3805
+ 500: '#6b7280',
3806
+ 600: '#4b5563',
3807
+ 700: '#374151',
3808
+ 800: '#1f2937',
3809
+ 900: '#111827',
3810
+ 950: '#030712',
3811
+ },
3812
+ primary: {
3813
+ color: '#1b8354',
3814
+ contrastColor: '#ffffff',
3815
+ hoverColor: '#243660',
3816
+ activeColor: '#0f1d35',
3817
+ },
3818
+ text: {
3819
+ color: '#1c252e',
3820
+ hoverColor: '#111827',
3821
+ mutedColor: '#888888',
3822
+ hoverMutedColor: '#6b7280',
3823
+ },
3824
+ formField: {
3825
+ background: '#ffffff',
3826
+ borderColor: '#9da4ae',
3827
+ hoverBorderColor: '#6b7280',
3828
+ focusBorderColor: '#1a2b4a',
3829
+ invalidBorderColor: '#c0392b',
3830
+ color: '#384250',
3831
+ placeholderColor: '#6C737F',
3832
+ floatLabelColor: '#6b7280',
3833
+ floatLabelFocusColor: '#1b8354',
3834
+ shadow: '0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05)',
3835
+ },
3836
+ content: {
3837
+ background: '#ffffff',
3838
+ hoverBackground: '#f9fafb',
3839
+ borderColor: '#d2d6db',
3840
+ color: '#1c252e',
3841
+ hoverColor: '#111827',
3842
+ },
3843
+ },
3844
+ dark: {
3845
+ surface: {
3846
+ 0: '#ffffff',
3847
+ 50: '#f8fafc',
3848
+ 100: '#f3f4f6',
3849
+ 200: '#e5e7eb',
3850
+ 300: '#d2d6db',
3851
+ 400: '#9da4ae',
3852
+ 500: '#6b7280',
3853
+ 600: '#4b5563',
3854
+ 700: '#374151',
3855
+ 800: '#1f2937',
3856
+ 900: '#111827',
3857
+ 950: '#030712',
3858
+ },
3859
+ primary: {
3860
+ color: '#1b8354',
3861
+ contrastColor: '#ffffff',
3862
+ hoverColor: '#243660',
3863
+ activeColor: '#0f1d35',
3864
+ },
3865
+ text: {
3866
+ color: '#1c252e',
3867
+ hoverColor: '#111827',
3868
+ mutedColor: '#888888',
3869
+ hoverMutedColor: '#6b7280',
3870
+ },
3871
+ formField: {
3872
+ background: '#ffffff',
3873
+ borderColor: '#9da4ae',
3874
+ hoverBorderColor: '#6b7280',
3875
+ focusBorderColor: '#1a2b4a',
3876
+ invalidBorderColor: '#c0392b',
3877
+ color: '#384250',
3878
+ placeholderColor: '#6C737F',
3879
+ floatLabelColor: '#6b7280',
3880
+ floatLabelFocusColor: '#1b8354',
3881
+ shadow: '0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05)',
3882
+ },
3883
+ content: {
3884
+ background: '#ffffff',
3885
+ hoverBackground: '#f9fafb',
3886
+ borderColor: '#d2d6db',
3887
+ color: '#1c252e',
3888
+ hoverColor: '#111827',
3889
+ },
3890
+ },
3891
+ },
3892
+ },
3893
+ });
3894
+
3895
+ class ExportPdfService {
3896
+ /**
3897
+ * Captures an HTML element as a pixel-perfect PDF and triggers a browser download.
3898
+ *
3899
+ * Fidelity improvements applied automatically:
3900
+ * - Waits for all web fonts (Arabic + Latin) to finish loading
3901
+ * - Resets scroll position so the full content is captured, not a mid-scroll slice
3902
+ * - Clones the DOM before capture and:
3903
+ * - Removes all elements marked `.no-pdf` (export buttons, toolbars, etc.)
3904
+ * - Converts sticky/fixed elements to relative so they don't repeat across pages
3905
+ * - Captures at the element's full scrollWidth so responsive breakpoints match screen
3906
+ *
3907
+ * @param element - The DOM element to capture
3908
+ * @param filename - Download filename without extension
3909
+ * @param options - Quality and layout overrides
3910
+ */
3911
+ async generate(element, filename, options = {}) {
3912
+ const { scale = 2, orientation = 'landscape', pageNumbers = true, jpegQuality } = options;
3913
+ // 1. Wait for all web fonts (Arabic, Latin) to be fully loaded
3914
+ await document.fonts.ready;
3915
+ // 2. Reset scroll so we always capture from the top of the element
3916
+ const prevScrollX = window.scrollX;
3917
+ const prevScrollY = window.scrollY;
3918
+ window.scrollTo(0, 0);
3919
+ const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([import('html2canvas'), import('jspdf')]);
3920
+ const bgColor = '#f8f9fb';
3921
+ // Measure the actual content height (not inflated by min-height: 100vh on children).
3922
+ const naturalHeight = this.measureNaturalHeight(element);
3923
+ const canvas = await html2canvas(element, {
3924
+ scale,
3925
+ useCORS: true,
3926
+ allowTaint: true,
3927
+ logging: false,
3928
+ backgroundColor: bgColor,
3929
+ // Keep windowWidth at the real browser viewport so CSS media queries evaluate
3930
+ // identically to what the user sees — prevents responsive grid from collapsing.
3931
+ windowWidth: window.innerWidth,
3932
+ windowHeight: window.innerHeight,
3933
+ // Capture full scrollable width (prevents horizontal content cut-off).
3934
+ width: element.scrollWidth,
3935
+ // Clip the canvas to the exact content height (no whitespace, no cut).
3936
+ height: naturalHeight,
3937
+ // Ignore elements the caller has marked as excluded from the PDF
3938
+ ignoreElements: el => el.classList.contains('no-pdf'),
3939
+ onclone: (doc, clonedEl) => {
3940
+ // html2canvas mirrors the canvas when dir="rtl" is on the document.
3941
+ // Removing the attribute from the CLONE only prevents the flip while
3942
+ // keeping all CSS direction/layout rules (which come from stylesheets).
3943
+ doc.documentElement.removeAttribute('dir');
3944
+ this.prepareClone(clonedEl);
3945
+ },
3946
+ });
3947
+ // Restore scroll position
3948
+ window.scrollTo(prevScrollX, prevScrollY);
3949
+ const imgFormat = jpegQuality !== undefined ? 'JPEG' : 'PNG';
3950
+ const imgData = jpegQuality !== undefined ? canvas.toDataURL('image/jpeg', jpegQuality) : canvas.toDataURL('image/png');
3951
+ // Convert canvas pixels → mm (96 dpi base, scale applied)
3952
+ const pxToMm = 25.4 / 96;
3953
+ const imgWidthMm = (canvas.width / scale) * pxToMm;
3954
+ const imgHeightMm = (canvas.height / scale) * pxToMm;
3955
+ if (pageNumbers) {
3956
+ // ── Multi-page A4 mode (with page numbers) ──────────────────────────────
3957
+ // Fit content width into A4 width, split across pages, no empty bottom page.
3958
+ const a4WidthMm = orientation === 'landscape' ? 297 : 210;
3959
+ const a4HeightMm = orientation === 'landscape' ? 210 : 297;
3960
+ // Scale image to fit A4 width
3961
+ const scaledImgHeightMm = (imgHeightMm * a4WidthMm) / imgWidthMm;
3962
+ const pdf = new jsPDF({ orientation, unit: 'mm', format: 'a4' });
3963
+ pdf.setProperties({ title: filename, creator: 'NCIM Dashboard' });
3964
+ let heightLeft = scaledImgHeightMm;
3965
+ let positionMm = 0;
3966
+ pdf.addImage(imgData, imgFormat, 0, positionMm, a4WidthMm, scaledImgHeightMm);
3967
+ heightLeft -= a4HeightMm;
3968
+ while (heightLeft > 0) {
3969
+ positionMm -= a4HeightMm;
3970
+ pdf.addPage();
3971
+ pdf.addImage(imgData, imgFormat, 0, positionMm, a4WidthMm, scaledImgHeightMm);
3972
+ heightLeft -= a4HeightMm;
3973
+ }
3974
+ // Page numbers — only on pages that have content
3975
+ const total = pdf.getNumberOfPages();
3976
+ for (let i = 1; i <= total; i++) {
3977
+ pdf.setPage(i);
3978
+ pdf.setFontSize(9);
3979
+ pdf.setTextColor(150);
3980
+ pdf.text(`${i} / ${total}`, a4WidthMm / 2, a4HeightMm - 4, { align: 'center' });
3981
+ }
3982
+ pdf.save(`${filename}.pdf`);
3983
+ }
3984
+ else {
3985
+ // ── Single-page A4-width mode (no white space at bottom) ────────────────
3986
+ // Scale content to fill A4 width; page height = scaled content height.
3987
+ // This ensures content fills the full page width with zero wasted space.
3988
+ const a4WidthMm = orientation === 'landscape' ? 297 : 210;
3989
+ const scaledHeightMm = (imgHeightMm * a4WidthMm) / imgWidthMm;
3990
+ const pdf = new jsPDF({ orientation, unit: 'mm', format: [a4WidthMm, scaledHeightMm] });
3991
+ pdf.setProperties({ title: filename, creator: 'NCIM Dashboard' });
3992
+ pdf.addImage(imgData, imgFormat, 0, 0, a4WidthMm, scaledHeightMm);
3993
+ pdf.save(`${filename}.pdf`);
3994
+ }
3995
+ }
3996
+ // ---------------------------------------------------------------------------
3997
+ // Private helpers
3998
+ // ---------------------------------------------------------------------------
3999
+ /**
4000
+ * Measures the natural content height of the element by temporarily applying
4001
+ * the same DOM transformations that prepareClone will apply to the captured
4002
+ * clone. This ensures the canvas height matches the clone's rendered height
4003
+ * exactly — preventing both whitespace (too tall) and cut content (too short).
4004
+ *
4005
+ * Transformations mirrored:
4006
+ * - Collapse min-height / height / max-height on root
4007
+ * - Collapse min-height on all children
4008
+ * - Expand overflow:auto/scroll containers to their scrollHeight
4009
+ */
4010
+ measureNaturalHeight(element) {
4011
+ const saved = [];
4012
+ const apply = (el, props) => {
4013
+ const orig = {};
4014
+ for (const [k, v] of Object.entries(props)) {
4015
+ orig[k] = el.style.getPropertyValue(k);
4016
+ el.style.setProperty(k, v);
4017
+ }
4018
+ saved.push({ el, props: orig });
4019
+ };
4020
+ // 1. Collapse root height constraints
4021
+ apply(element, {
4022
+ height: 'auto',
4023
+ 'min-height': '0',
4024
+ 'max-height': 'none',
4025
+ overflow: 'visible',
4026
+ });
4027
+ // 2. Collapse min-height on children + expand scroll containers
4028
+ element.querySelectorAll('*').forEach(el => {
4029
+ const cs = getComputedStyle(el);
4030
+ if (cs.minHeight && cs.minHeight !== '0px') {
4031
+ apply(el, { 'min-height': '0' });
4032
+ }
4033
+ const hasScroll = cs.overflow === 'auto' || cs.overflow === 'scroll' || cs.overflowY === 'auto' || cs.overflowY === 'scroll';
4034
+ if (hasScroll && el.scrollHeight > el.clientHeight) {
4035
+ apply(el, {
4036
+ height: `${el.scrollHeight}px`,
4037
+ overflow: 'visible',
4038
+ overflowY: 'visible',
4039
+ 'max-height': 'none',
4040
+ });
4041
+ }
4042
+ });
4043
+ // 3. Read the resulting scroll height (single forced reflow)
4044
+ const result = element.scrollHeight;
4045
+ // 4. Restore all styles in reverse order
4046
+ for (let i = saved.length - 1; i >= 0; i--) {
4047
+ const { el, props } = saved[i];
4048
+ for (const [k, v] of Object.entries(props)) {
4049
+ if (v) {
4050
+ el.style.setProperty(k, v);
4051
+ }
4052
+ else {
4053
+ el.style.removeProperty(k);
4054
+ }
4055
+ }
4056
+ }
4057
+ return result;
4058
+ }
4059
+ /** Walk the cloned DOM and neutralise elements that break full-height capture. */
4060
+ prepareClone(root) {
4061
+ // Remove elements explicitly excluded by the caller
4062
+ root.querySelectorAll('.no-pdf').forEach(el => el.remove());
4063
+ // Collapse root to natural content height — removes viewport-sized min-height gaps
4064
+ root.style.height = 'auto';
4065
+ root.style.minHeight = '0';
4066
+ root.style.maxHeight = 'none';
4067
+ root.style.overflow = 'visible';
4068
+ root.querySelectorAll('*').forEach(el => {
4069
+ const style = getComputedStyle(el);
4070
+ // Convert sticky/fixed to relative so headers don't repeat across pages
4071
+ if (style.position === 'sticky' || style.position === 'fixed') {
4072
+ el.style.position = 'relative';
4073
+ }
4074
+ // Collapse min-height on children that might add empty padding space
4075
+ if (style.minHeight && style.minHeight !== '0px') {
4076
+ el.style.minHeight = '0';
4077
+ }
4078
+ // Expand internal scroll containers so their hidden content is captured
4079
+ const hasScroll = style.overflow === 'auto' ||
4080
+ style.overflow === 'scroll' ||
4081
+ style.overflowY === 'auto' ||
4082
+ style.overflowY === 'scroll';
4083
+ if (hasScroll && el.scrollHeight > el.clientHeight) {
4084
+ el.style.height = `${el.scrollHeight}px`;
4085
+ el.style.overflow = 'visible';
4086
+ el.style.overflowY = 'visible';
4087
+ el.style.maxHeight = 'none';
4088
+ }
4089
+ // Force content-visibility: visible so off-screen sections are rendered
4090
+ if (style.contentVisibility === 'auto') {
4091
+ el.style.contentVisibility = 'visible';
4092
+ }
4093
+ });
4094
+ }
4095
+ /**
4096
+ * Resolve the effective background color of the element.
4097
+ * Falls back to white if transparent (avoids black PDF background).
4098
+ */
4099
+ resolveBackground(element) {
4100
+ let el = element;
4101
+ while (el) {
4102
+ const bg = getComputedStyle(el).backgroundColor;
4103
+ if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
4104
+ return bg;
4105
+ }
4106
+ el = el.parentElement;
4107
+ }
4108
+ return '#ffffff';
4109
+ }
4110
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ExportPdfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4111
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ExportPdfService, providedIn: 'root' });
4112
+ }
4113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ExportPdfService, decorators: [{
4114
+ type: Injectable,
4115
+ args: [{ providedIn: 'root' }]
4116
+ }] });
2
4117
 
3
4118
  /**
4
4119
  * Generated bundle index. Do not edit.
5
4120
  */
4121
+
4122
+ export { API_ENDPOINTS, ASSET_PATHS, AlphaNumericDirective, ApiService, ArabicOnlyDirective, AuthService, AutofocusDirective, BUSINESS_STEPS, ButtonSize, ButtonType, ButtonVariant, CACHE_DEFAULTS, CACHE_ENABLED, CACHE_PATH_DEPTH, CACHE_TTL, CHART_COLORS, ENTITY_DETAILS_DEFAULTS, ENTITY_DETAILS_OPTIONS, ENTITY_TYPE_OPTIONS, ENVIRONMENT, ERROR_KEYS, ERROR_MESSAGE_MAX_LENGTH, EmailCharsDirective, EnglishNumberPipe, EnglishOnlyDirective, EntityType, ExportPdfService, FALLBACK_LANGUAGE, GENERATED_ICON_NAMES, GOVERNMENT_STEPS, GlobalErrorHandler, HTTP_HEADERS, HasPermissionDirective, HttpCacheService, HttpErrorKey, I18N_CONFIG, ICONS, IDEMPOTENT_METHODS, INPUT_TYPES, INSPECTION_STEPS, ImgFallbackDirective, LOADING_DELAY_MS, LOGOUT_DEBOUNCE_MS, Language, LanguageService, LayoutWidth, LoadingService, LocalizedDatePipe, MAP_ACTION_ICONS, MAP_CONFIG, MAP_LEGEND_CONFIG, MAX_FIELD_ERRORS_SHOWN, MAX_RETRY_COUNT, MapLegendStatus, MarkerStatus, NcimPreset, NoWhitespaceDirective, NumbersOnlyDirective, OidcAuthService, PACKAGES, PACKAGE_FEATURES, PHONE_CONFIG, PLACEHOLDER_KEYS, PRIMENG_LOCALE, PUBLIC_API_PATHS, PackageType, PhoneOnlyDirective, PluralTranslateCompiler, PreventPasteDirective, REGEX, REGISTRATION_DEFAULTS, REGISTRATION_STEPS, RETRYABLE_STATUS_CODES, RETRY_DEFAULTS, ROUTE_PATHS, ROUTE_SEGMENTS, ReplacePipe, SIDEBAR_CONFIG, SIDEBAR_DEFAULT_STEP, SKIP_AUTH, SKIP_CONNECTIVITY, SKIP_ERROR_HANDLING, SKIP_LOADING, SKIP_RETRY, SSO_CALLBACK_PARAMS_KEY, SSO_REDIRECT_URL_KEY, STORAGE_KEYS, SelectOnClickDirective, SeoService, TIME, TOAST_DEDUP_MAX_SIZE, TOAST_DEDUP_MS, TOAST_DURATION, ToUppercaseDirective, ToastService, TrendDirection, TrimInputDirective, VALIDATION_LENGTHS, VALIDATION_PATTERNS, alphanumericValidator, arabicOnlyValidator, authGuard, authInterceptor, baseUrlInterceptor, cacheInterceptor, confirmedValidator, connectivityInterceptor, crNumberValidator, createApiErrorHandler, dateMaxValidator, dateMinValidator, emailValidator, englishOnlyValidator, environments, errorInterceptor, guestGuard, ibanValidator, idNumberValidator, identityNumberValidator, initFormData, lettersOnlyValidator, loadingInterceptor, markSubmittedAndValidate, minSelectedValidator, noWhitespaceValidator, numbersOnlyValidator, parseDate, passwordMatchValidator, passwordStrengthValidator, patternValidator, permissionGuard, phoneValidator, provideEnvironment, retryInterceptor, saudiMobile05Validator, saudiMobileValidator, showApiErrorToast, syncFormWithLoading, toFormData, toHttpParams, urlValidator, useFormInitData };
6
4123
  //# sourceMappingURL=ncim-sdk-core.mjs.map