@masterteam/system-settings 0.0.17 → 0.0.18

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.
@@ -0,0 +1,2707 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
3
+ import * as i0 from '@angular/core';
4
+ import { input, ChangeDetectionStrategy, Component, inject, computed, effect, output, signal, InjectionToken, makeEnvironmentProviders, Injectable, DestroyRef } from '@angular/core';
5
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import * as i1 from '@angular/forms';
7
+ import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
8
+ import { Router } from '@angular/router';
9
+ import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
10
+ import { Breadcrumb } from '@masterteam/components/breadcrumb';
11
+ import { Button } from '@masterteam/components/button';
12
+ import { Page } from '@masterteam/components/page';
13
+ import { ModalService } from '@masterteam/components/modal';
14
+ import { ToastService } from '@masterteam/components/toast';
15
+ import * as i1$1 from 'primeng/skeleton';
16
+ import { SkeletonModule } from 'primeng/skeleton';
17
+ import { Card } from '@masterteam/components/card';
18
+ import { MultiSelectField } from '@masterteam/components/multi-select-field';
19
+ import { SelectField } from '@masterteam/components/select-field';
20
+ import { RadioCardsField } from '@masterteam/components/radio-cards-field';
21
+ import { ToggleField } from '@masterteam/components/toggle-field';
22
+ import { ModalRef } from '@masterteam/components/dialog';
23
+ import { DateField } from '@masterteam/components/date-field';
24
+ import { TextField } from '@masterteam/components/text-field';
25
+ import { Table } from '@masterteam/components/table';
26
+ import { NumberField } from '@masterteam/components/number-field';
27
+ import { TextareaField } from '@masterteam/components/textarea-field';
28
+ import { of, map, tap, catchError, throwError, finalize, shareReplay, mergeMap, switchMap, forkJoin, timer } from 'rxjs';
29
+ import { EntityStatus } from '@masterteam/components/entities';
30
+
31
+ /**
32
+ * "Experience & Appearance" — product-wide application behaviour that is not
33
+ * tied to a specific feature area: the explore landing view and whether the
34
+ * topbar design switcher is offered. More entries are expected here.
35
+ */
36
+ class SystemSettingsGeneralConfigSection {
37
+ generalConfigurationForm = input.required(...(ngDevMode ? [{ debugName: "generalConfigurationForm" }] : /* istanbul ignore next */ []));
38
+ exploreDefaultViewOptions = input([], ...(ngDevMode ? [{ debugName: "exploreDefaultViewOptions" }] : /* istanbul ignore next */ []));
39
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsGeneralConfigSection, deps: [], target: i0.ɵɵFactoryTarget.Component });
40
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: SystemSettingsGeneralConfigSection, isStandalone: true, selector: "mt-system-settings-general-config-section", inputs: { generalConfigurationForm: { classPropertyName: "generalConfigurationForm", publicName: "generalConfigurationForm", isSignal: true, isRequired: true, transformFunction: null }, exploreDefaultViewOptions: { classPropertyName: "exploreDefaultViewOptions", publicName: "exploreDefaultViewOptions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\r\n <mt-card class=\"block\">\r\n <div [formGroup]=\"generalConfigurationForm()\" class=\"space-y-5\">\r\n <div class=\"space-y-1\">\r\n <h4 class=\"text-base font-semibold text-neutral-900\">\r\n {{ t(\"general-configuration-title\") }}\r\n </h4>\r\n <p class=\"text-sm text-neutral-600\">\r\n {{ t(\"general-configuration-description\") }}\r\n </p>\r\n </div>\r\n\r\n <mt-radio-cards-field\r\n formControlName=\"defaultExploreView\"\r\n [options]=\"exploreDefaultViewOptions()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'id'\"\r\n [label]=\"t('explore-default-view')\"\r\n [hint]=\"t('explore-default-view-hint')\"\r\n [required]=\"true\"\r\n size=\"small\"\r\n />\r\n\r\n <div class=\"space-y-1\">\r\n <mt-toggle-field\r\n formControlName=\"showDesignSwitcher\"\r\n [label]=\"t('design-switcher')\"\r\n />\r\n <p class=\"text-sm text-neutral-600\">\r\n {{ t(\"design-switcher-hint\") }}\r\n </p>\r\n </div>\r\n </div>\r\n </mt-card>\r\n</ng-container>\r\n", styles: [":host{display:block;height:100%}mt-card{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: RadioCardsField, selector: "mt-radio-cards-field", inputs: ["circle", "label", "hint", "readonly", "required", "color", "size", "optionLabel", "optionValue", "options"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
41
+ }
42
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsGeneralConfigSection, decorators: [{
43
+ type: Component,
44
+ args: [{ selector: 'mt-system-settings-general-config-section', imports: [
45
+ ReactiveFormsModule,
46
+ TranslocoDirective,
47
+ Card,
48
+ RadioCardsField,
49
+ ToggleField,
50
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\r\n <mt-card class=\"block\">\r\n <div [formGroup]=\"generalConfigurationForm()\" class=\"space-y-5\">\r\n <div class=\"space-y-1\">\r\n <h4 class=\"text-base font-semibold text-neutral-900\">\r\n {{ t(\"general-configuration-title\") }}\r\n </h4>\r\n <p class=\"text-sm text-neutral-600\">\r\n {{ t(\"general-configuration-description\") }}\r\n </p>\r\n </div>\r\n\r\n <mt-radio-cards-field\r\n formControlName=\"defaultExploreView\"\r\n [options]=\"exploreDefaultViewOptions()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'id'\"\r\n [label]=\"t('explore-default-view')\"\r\n [hint]=\"t('explore-default-view-hint')\"\r\n [required]=\"true\"\r\n size=\"small\"\r\n />\r\n\r\n <div class=\"space-y-1\">\r\n <mt-toggle-field\r\n formControlName=\"showDesignSwitcher\"\r\n [label]=\"t('design-switcher')\"\r\n />\r\n <p class=\"text-sm text-neutral-600\">\r\n {{ t(\"design-switcher-hint\") }}\r\n </p>\r\n </div>\r\n </div>\r\n </mt-card>\r\n</ng-container>\r\n", styles: [":host{display:block;height:100%}mt-card{height:100%}\n"] }]
51
+ }], propDecorators: { generalConfigurationForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "generalConfigurationForm", required: true }] }], exploreDefaultViewOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "exploreDefaultViewOptions", required: false }] }] } });
52
+
53
+ class SystemSettingsGeneralSection {
54
+ generalConfigurationForm = input.required(...(ngDevMode ? [{ debugName: "generalConfigurationForm" }] : /* istanbul ignore next */ []));
55
+ timezoneForm = input.required(...(ngDevMode ? [{ debugName: "timezoneForm" }] : /* istanbul ignore next */ []));
56
+ currencyForm = input.required(...(ngDevMode ? [{ debugName: "currencyForm" }] : /* istanbul ignore next */ []));
57
+ weekendForm = input.required(...(ngDevMode ? [{ debugName: "weekendForm" }] : /* istanbul ignore next */ []));
58
+ exploreDefaultViewOptions = input([], ...(ngDevMode ? [{ debugName: "exploreDefaultViewOptions" }] : /* istanbul ignore next */ []));
59
+ dateFormatOptions = input([], ...(ngDevMode ? [{ debugName: "dateFormatOptions" }] : /* istanbul ignore next */ []));
60
+ timeFormatOptions = input([], ...(ngDevMode ? [{ debugName: "timeFormatOptions" }] : /* istanbul ignore next */ []));
61
+ currencyOptions = input([], ...(ngDevMode ? [{ debugName: "currencyOptions" }] : /* istanbul ignore next */ []));
62
+ currencyFormatOptions = input([], ...(ngDevMode ? [{ debugName: "currencyFormatOptions" }] : /* istanbul ignore next */ []));
63
+ weekendOptions = input([], ...(ngDevMode ? [{ debugName: "weekendOptions" }] : /* istanbul ignore next */ []));
64
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsGeneralSection, deps: [], target: i0.ɵɵFactoryTarget.Component });
65
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: SystemSettingsGeneralSection, isStandalone: true, selector: "mt-system-settings-general-section", inputs: { generalConfigurationForm: { classPropertyName: "generalConfigurationForm", publicName: "generalConfigurationForm", isSignal: true, isRequired: true, transformFunction: null }, timezoneForm: { classPropertyName: "timezoneForm", publicName: "timezoneForm", isSignal: true, isRequired: true, transformFunction: null }, currencyForm: { classPropertyName: "currencyForm", publicName: "currencyForm", isSignal: true, isRequired: true, transformFunction: null }, weekendForm: { classPropertyName: "weekendForm", publicName: "weekendForm", isSignal: true, isRequired: true, transformFunction: null }, exploreDefaultViewOptions: { classPropertyName: "exploreDefaultViewOptions", publicName: "exploreDefaultViewOptions", isSignal: true, isRequired: false, transformFunction: null }, dateFormatOptions: { classPropertyName: "dateFormatOptions", publicName: "dateFormatOptions", isSignal: true, isRequired: false, transformFunction: null }, timeFormatOptions: { classPropertyName: "timeFormatOptions", publicName: "timeFormatOptions", isSignal: true, isRequired: false, transformFunction: null }, currencyOptions: { classPropertyName: "currencyOptions", publicName: "currencyOptions", isSignal: true, isRequired: false, transformFunction: null }, currencyFormatOptions: { classPropertyName: "currencyFormatOptions", publicName: "currencyFormatOptions", isSignal: true, isRequired: false, transformFunction: null }, weekendOptions: { classPropertyName: "weekendOptions", publicName: "weekendOptions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div class=\"grid gap-6 xl:grid-cols-3\">\n <mt-card class=\"block\">\n <div class=\"space-y-4\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"date-time-weekend-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"date-time-weekend-description\") }}\n </p>\n </div>\n\n <div [formGroup]=\"timezoneForm()\" class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n formControlName=\"dateFormat\"\n [options]=\"dateFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('date-format')\"\n [placeholder]=\"t('date-format-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-select-field\n formControlName=\"timeFormat\"\n [options]=\"timeFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('time-format')\"\n [placeholder]=\"t('time-format-placeholder')\"\n [required]=\"true\"\n />\n </div>\n\n <div [formGroup]=\"weekendForm()\">\n <mt-multi-select-field\n formControlName=\"days\"\n [options]=\"weekendOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'dayNumber'\"\n [label]=\"t('weekend-days')\"\n [placeholder]=\"t('weekend-placeholder')\"\n [display]=\"'chip'\"\n [filter]=\"false\"\n [showClear]=\"true\"\n [maxSelectedLabels]=\"3\"\n />\n </div>\n </div>\n </mt-card>\n\n <mt-card class=\"block\">\n <div [formGroup]=\"currencyForm()\" class=\"space-y-4\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"currency-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"currency-description\") }}\n </p>\n </div>\n\n <mt-select-field\n formControlName=\"code\"\n [options]=\"currencyOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('currency')\"\n [placeholder]=\"t('currency-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-select-field\n formControlName=\"format\"\n [options]=\"currencyFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('currency-format')\"\n [placeholder]=\"t('currency-format-placeholder')\"\n [required]=\"true\"\n />\n </div>\n </mt-card>\n\n <mt-system-settings-general-config-section\n class=\"block\"\n [generalConfigurationForm]=\"generalConfigurationForm()\"\n [exploreDefaultViewOptions]=\"exploreDefaultViewOptions()\"\n />\n </div>\n</ng-container>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: MultiSelectField, selector: "mt-multi-select-field", inputs: ["field", "label", "placeholder", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "display", "required", "maxSelectedLabels", "group", "virtualScroll", "virtualScrollItemSize", "optionGroupLabel", "optionGroupChildren", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape"], outputs: ["onChange"] }, { kind: "component", type: SystemSettingsGeneralConfigSection, selector: "mt-system-settings-general-config-section", inputs: ["generalConfigurationForm", "exploreDefaultViewOptions"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
66
+ }
67
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsGeneralSection, decorators: [{
68
+ type: Component,
69
+ args: [{ selector: 'mt-system-settings-general-section', imports: [
70
+ ReactiveFormsModule,
71
+ TranslocoDirective,
72
+ Card,
73
+ SelectField,
74
+ MultiSelectField,
75
+ SystemSettingsGeneralConfigSection,
76
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div class=\"grid gap-6 xl:grid-cols-3\">\n <mt-card class=\"block\">\n <div class=\"space-y-4\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"date-time-weekend-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"date-time-weekend-description\") }}\n </p>\n </div>\n\n <div [formGroup]=\"timezoneForm()\" class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n formControlName=\"dateFormat\"\n [options]=\"dateFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('date-format')\"\n [placeholder]=\"t('date-format-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-select-field\n formControlName=\"timeFormat\"\n [options]=\"timeFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('time-format')\"\n [placeholder]=\"t('time-format-placeholder')\"\n [required]=\"true\"\n />\n </div>\n\n <div [formGroup]=\"weekendForm()\">\n <mt-multi-select-field\n formControlName=\"days\"\n [options]=\"weekendOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'dayNumber'\"\n [label]=\"t('weekend-days')\"\n [placeholder]=\"t('weekend-placeholder')\"\n [display]=\"'chip'\"\n [filter]=\"false\"\n [showClear]=\"true\"\n [maxSelectedLabels]=\"3\"\n />\n </div>\n </div>\n </mt-card>\n\n <mt-card class=\"block\">\n <div [formGroup]=\"currencyForm()\" class=\"space-y-4\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"currency-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"currency-description\") }}\n </p>\n </div>\n\n <mt-select-field\n formControlName=\"code\"\n [options]=\"currencyOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('currency')\"\n [placeholder]=\"t('currency-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-select-field\n formControlName=\"format\"\n [options]=\"currencyFormatOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('currency-format')\"\n [placeholder]=\"t('currency-format-placeholder')\"\n [required]=\"true\"\n />\n </div>\n </mt-card>\n\n <mt-system-settings-general-config-section\n class=\"block\"\n [generalConfigurationForm]=\"generalConfigurationForm()\"\n [exploreDefaultViewOptions]=\"exploreDefaultViewOptions()\"\n />\n </div>\n</ng-container>\n", styles: [":host{display:block}\n"] }]
77
+ }], propDecorators: { generalConfigurationForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "generalConfigurationForm", required: true }] }], timezoneForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "timezoneForm", required: true }] }], currencyForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyForm", required: true }] }], weekendForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekendForm", required: true }] }], exploreDefaultViewOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "exploreDefaultViewOptions", required: false }] }], dateFormatOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormatOptions", required: false }] }], timeFormatOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormatOptions", required: false }] }], currencyOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyOptions", required: false }] }], currencyFormatOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyFormatOptions", required: false }] }], weekendOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekendOptions", required: false }] }] } });
78
+
79
+ class SystemSettingsHolidayEditor {
80
+ ref = inject(ModalRef);
81
+ modal = inject(ModalService);
82
+ holiday = input(null, ...(ngDevMode ? [{ debugName: "holiday" }] : /* istanbul ignore next */ []));
83
+ isEditMode = input(false, ...(ngDevMode ? [{ debugName: "isEditMode" }] : /* istanbul ignore next */ []));
84
+ form = new FormGroup({
85
+ label: new FormControl('', {
86
+ nonNullable: true,
87
+ validators: [Validators.required],
88
+ }),
89
+ from: new FormControl(null, {
90
+ validators: [Validators.required],
91
+ }),
92
+ to: new FormControl(null, {
93
+ validators: [Validators.required],
94
+ }),
95
+ }, {
96
+ validators: [holidayRangeValidator()],
97
+ });
98
+ actionLabel = computed(() => this.isEditMode() ? 'update-holiday' : 'add-holiday', ...(ngDevMode ? [{ debugName: "actionLabel" }] : /* istanbul ignore next */ []));
99
+ fromValue = toSignal(this.form.controls.from.valueChanges, {
100
+ initialValue: this.form.controls.from.value,
101
+ });
102
+ toPickerInputs = computed(() => {
103
+ const from = this.fromValue();
104
+ return from instanceof Date ? { minDate: from } : {};
105
+ }, ...(ngDevMode ? [{ debugName: "toPickerInputs" }] : /* istanbul ignore next */ []));
106
+ constructor() {
107
+ effect(() => {
108
+ const holiday = this.holiday();
109
+ this.form.reset({
110
+ label: holiday?.label ?? '',
111
+ from: holiday?.from ?? null,
112
+ to: holiday?.to ?? null,
113
+ }, { emitEvent: false });
114
+ this.form.markAsPristine();
115
+ this.form.markAsUntouched();
116
+ });
117
+ }
118
+ onSave() {
119
+ this.form.markAllAsTouched();
120
+ if (this.form.invalid) {
121
+ return;
122
+ }
123
+ const value = this.form.getRawValue();
124
+ if (!(value.from instanceof Date) || !(value.to instanceof Date)) {
125
+ return;
126
+ }
127
+ this.ref.close({
128
+ label: value.label.trim(),
129
+ from: value.from,
130
+ to: value.to,
131
+ });
132
+ }
133
+ onCancel() {
134
+ this.ref.close(null);
135
+ }
136
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsHolidayEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
137
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: SystemSettingsHolidayEditor, isStandalone: true, selector: "mt-system-settings-holiday-editor", inputs: { holiday: { classPropertyName: "holiday", publicName: "holiday", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div [class]=\"' ' + (modal.contentClass || '')\">\n <form [formGroup]=\"form\" class=\"space-y-4 p-3\" (ngSubmit)=\"onSave()\">\n <mt-text-field\n formControlName=\"label\"\n [label]=\"t('holiday-name')\"\n [placeholder]=\"t('holiday-name-placeholder')\"\n [required]=\"true\"\n />\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <div class=\"grid gap-1\">\n <mt-date-field\n formControlName=\"from\"\n [label]=\"t('from-date')\"\n [placeholder]=\"t('from-date')\"\n [showIcon]=\"true\"\n [required]=\"true\"\n />\n </div>\n\n <div class=\"grid gap-1\">\n <mt-date-field\n formControlName=\"to\"\n [label]=\"t('to-date')\"\n [placeholder]=\"t('to-date')\"\n [showIcon]=\"true\"\n [required]=\"true\"\n [pInputs]=\"toPickerInputs()\"\n />\n </div>\n </div>\n\n @if (form.errors?.[\"invalidRange\"] && (form.dirty || form.touched)) {\n <div class=\"text-sm text-red-600\">\n {{ t(\"holiday-range-error\") }}\n </div>\n }\n </form>\n </div>\n <div [class]=\"modal.footerClass\">\n <mt-button\n [label]=\"t('cancel')\"\n variant=\"outlined\"\n type=\"button\"\n (onClick)=\"onCancel()\"\n />\n <mt-button [label]=\"t(actionLabel())\" type=\"button\" (onClick)=\"onSave()\" />\n </div>\n</ng-container>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: DateField, selector: "mt-date-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "showIcon", "showOnFocus", "showClear", "showTime", "timeOnly", "minDate", "maxDate", "pInputs", "required"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
138
+ }
139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsHolidayEditor, decorators: [{
140
+ type: Component,
141
+ args: [{ selector: 'mt-system-settings-holiday-editor', imports: [
142
+ ReactiveFormsModule,
143
+ TranslocoDirective,
144
+ Button,
145
+ DateField,
146
+ TextField,
147
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div [class]=\"' ' + (modal.contentClass || '')\">\n <form [formGroup]=\"form\" class=\"space-y-4 p-3\" (ngSubmit)=\"onSave()\">\n <mt-text-field\n formControlName=\"label\"\n [label]=\"t('holiday-name')\"\n [placeholder]=\"t('holiday-name-placeholder')\"\n [required]=\"true\"\n />\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <div class=\"grid gap-1\">\n <mt-date-field\n formControlName=\"from\"\n [label]=\"t('from-date')\"\n [placeholder]=\"t('from-date')\"\n [showIcon]=\"true\"\n [required]=\"true\"\n />\n </div>\n\n <div class=\"grid gap-1\">\n <mt-date-field\n formControlName=\"to\"\n [label]=\"t('to-date')\"\n [placeholder]=\"t('to-date')\"\n [showIcon]=\"true\"\n [required]=\"true\"\n [pInputs]=\"toPickerInputs()\"\n />\n </div>\n </div>\n\n @if (form.errors?.[\"invalidRange\"] && (form.dirty || form.touched)) {\n <div class=\"text-sm text-red-600\">\n {{ t(\"holiday-range-error\") }}\n </div>\n }\n </form>\n </div>\n <div [class]=\"modal.footerClass\">\n <mt-button\n [label]=\"t('cancel')\"\n variant=\"outlined\"\n type=\"button\"\n (onClick)=\"onCancel()\"\n />\n <mt-button [label]=\"t(actionLabel())\" type=\"button\" (onClick)=\"onSave()\" />\n </div>\n</ng-container>\n", styles: [":host{display:block}\n"] }]
148
+ }], ctorParameters: () => [], propDecorators: { holiday: [{ type: i0.Input, args: [{ isSignal: true, alias: "holiday", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: false }] }] } });
149
+ function holidayRangeValidator() {
150
+ return (control) => {
151
+ const from = control.get('from')?.value;
152
+ const to = control.get('to')?.value;
153
+ if (!from || !to) {
154
+ return null;
155
+ }
156
+ return from <= to ? null : { invalidRange: true };
157
+ };
158
+ }
159
+
160
+ class SystemSettingsHolidaysSection {
161
+ transloco = inject(TranslocoService);
162
+ holidays = input([], ...(ngDevMode ? [{ debugName: "holidays" }] : /* istanbul ignore next */ []));
163
+ addHoliday = output();
164
+ editHoliday = output();
165
+ deleteHoliday = output();
166
+ columns = computed(() => [
167
+ {
168
+ key: 'label',
169
+ label: this.transloco.translate('system-settings.holiday-name'),
170
+ },
171
+ {
172
+ key: 'from',
173
+ label: this.transloco.translate('system-settings.from-date'),
174
+ },
175
+ {
176
+ key: 'to',
177
+ label: this.transloco.translate('system-settings.to-date'),
178
+ },
179
+ ], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
180
+ rowActions = computed(() => [
181
+ {
182
+ icon: 'general.edit-01',
183
+ tooltip: this.transloco.translate('system-settings.edit-holiday'),
184
+ color: 'secondary',
185
+ variant: 'outlined',
186
+ action: (row) => this.editHoliday.emit(row.rowIndex),
187
+ },
188
+ {
189
+ icon: 'general.trash-01',
190
+ tooltip: this.transloco.translate('system-settings.remove-holiday'),
191
+ color: 'danger',
192
+ variant: 'outlined',
193
+ action: (row) => this.deleteHoliday.emit(row.rowIndex),
194
+ },
195
+ ], ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
196
+ hasHolidays = computed(() => this.holidays().length > 0, ...(ngDevMode ? [{ debugName: "hasHolidays" }] : /* istanbul ignore next */ []));
197
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsHolidaysSection, deps: [], target: i0.ɵɵFactoryTarget.Component });
198
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: SystemSettingsHolidaysSection, isStandalone: true, selector: "mt-system-settings-holidays-section", inputs: { holidays: { classPropertyName: "holidays", publicName: "holidays", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addHoliday: "addHoliday", editHoliday: "editHoliday", deleteHoliday: "deleteHoliday" }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <mt-card class=\"block\">\n <div class=\"space-y-6\">\n <div\n class=\"flex flex-col gap-4 md:flex-row md:items-start md:justify-between\"\n >\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"holidays-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"holidays-description\") }}\n </p>\n </div>\n\n <mt-button\n [label]=\"t('add-holiday')\"\n icon=\"general.plus\"\n (onClick)=\"addHoliday.emit()\"\n />\n </div>\n\n @if (hasHolidays()) {\n <mt-table\n noCard\n [data]=\"holidays()\"\n [columns]=\"columns()\"\n [rowActions]=\"rowActions()\"\n [generalSearch]=\"false\"\n [showFilters]=\"false\"\n [dataKey]=\"'rowKey'\"\n storageKey=\"admin-system-settings-holidays-table\"\n >\n </mt-table>\n } @else {\n <div\n class=\"rounded-xl border border-dashed border-neutral-300 bg-neutral-50 px-6 py-10 text-center\"\n >\n <h5 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"no-holidays-title\") }}\n </h5>\n <p class=\"mt-2 text-sm text-neutral-600\">\n {{ t(\"no-holidays-description\") }}\n </p>\n </div>\n }\n </div>\n </mt-card>\n</ng-container>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
199
+ }
200
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsHolidaysSection, decorators: [{
201
+ type: Component,
202
+ args: [{ selector: 'mt-system-settings-holidays-section', imports: [TranslocoDirective, Button, Card, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <mt-card class=\"block\">\n <div class=\"space-y-6\">\n <div\n class=\"flex flex-col gap-4 md:flex-row md:items-start md:justify-between\"\n >\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"holidays-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"holidays-description\") }}\n </p>\n </div>\n\n <mt-button\n [label]=\"t('add-holiday')\"\n icon=\"general.plus\"\n (onClick)=\"addHoliday.emit()\"\n />\n </div>\n\n @if (hasHolidays()) {\n <mt-table\n noCard\n [data]=\"holidays()\"\n [columns]=\"columns()\"\n [rowActions]=\"rowActions()\"\n [generalSearch]=\"false\"\n [showFilters]=\"false\"\n [dataKey]=\"'rowKey'\"\n storageKey=\"admin-system-settings-holidays-table\"\n >\n </mt-table>\n } @else {\n <div\n class=\"rounded-xl border border-dashed border-neutral-300 bg-neutral-50 px-6 py-10 text-center\"\n >\n <h5 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"no-holidays-title\") }}\n </h5>\n <p class=\"mt-2 text-sm text-neutral-600\">\n {{ t(\"no-holidays-description\") }}\n </p>\n </div>\n }\n </div>\n </mt-card>\n</ng-container>\n", styles: [":host{display:block}\n"] }]
203
+ }], propDecorators: { holidays: [{ type: i0.Input, args: [{ isSignal: true, alias: "holidays", required: false }] }], addHoliday: [{ type: i0.Output, args: ["addHoliday"] }], editHoliday: [{ type: i0.Output, args: ["editHoliday"] }], deleteHoliday: [{ type: i0.Output, args: ["deleteHoliday"] }] } });
204
+
205
+ class SystemSettingsTimeSheetSection {
206
+ policyForm = input.required(...(ngDevMode ? [{ debugName: "policyForm" }] : /* istanbul ignore next */ []));
207
+ globalWorkingHoursForm = input.required(...(ngDevMode ? [{ debugName: "globalWorkingHoursForm" }] : /* istanbul ignore next */ []));
208
+ dayOptions = input([], ...(ngDevMode ? [{ debugName: "dayOptions" }] : /* istanbul ignore next */ []));
209
+ frequencyOptions = input([], ...(ngDevMode ? [{ debugName: "frequencyOptions" }] : /* istanbul ignore next */ []));
210
+ timezoneOptions = input([], ...(ngDevMode ? [{ debugName: "timezoneOptions" }] : /* istanbul ignore next */ []));
211
+ maxDailyBelowDaily = input(false, ...(ngDevMode ? [{ debugName: "maxDailyBelowDaily" }] : /* istanbul ignore next */ []));
212
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsTimeSheetSection, deps: [], target: i0.ɵɵFactoryTarget.Component });
213
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: SystemSettingsTimeSheetSection, isStandalone: true, selector: "mt-system-settings-timesheet-section", inputs: { policyForm: { classPropertyName: "policyForm", publicName: "policyForm", isSignal: true, isRequired: true, transformFunction: null }, globalWorkingHoursForm: { classPropertyName: "globalWorkingHoursForm", publicName: "globalWorkingHoursForm", isSignal: true, isRequired: true, transformFunction: null }, dayOptions: { classPropertyName: "dayOptions", publicName: "dayOptions", isSignal: true, isRequired: false, transformFunction: null }, frequencyOptions: { classPropertyName: "frequencyOptions", publicName: "frequencyOptions", isSignal: true, isRequired: false, transformFunction: null }, timezoneOptions: { classPropertyName: "timezoneOptions", publicName: "timezoneOptions", isSignal: true, isRequired: false, transformFunction: null }, maxDailyBelowDaily: { classPropertyName: "maxDailyBelowDaily", publicName: "maxDailyBelowDaily", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div class=\"grid gap-6 xl:grid-cols-2 mb-6\">\n <mt-card class=\"block\">\n <div [formGroup]=\"policyForm()\" class=\"space-y-5\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"general-card-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"general-card-description\") }}\n </p>\n </div>\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n formControlName=\"frequency\"\n [options]=\"frequencyOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('frequency')\"\n [placeholder]=\"t('frequency-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-number-field\n formControlName=\"maxDailyHours\"\n [label]=\"t('max-daily-hours')\"\n [placeholder]=\"t('max-daily-hours-placeholder')\"\n [min]=\"1\"\n [max]=\"24\"\n [required]=\"true\"\n [hint]=\"maxDailyBelowDaily() ? t('max-daily-below-daily') : ''\"\n />\n\n <mt-number-field\n formControlName=\"submissionDeadlineDays\"\n [label]=\"t('submission-deadline-days')\"\n [placeholder]=\"t('submission-deadline-days-placeholder')\"\n [min]=\"0\"\n [max]=\"90\"\n [required]=\"true\"\n [hint]=\"t('submission-deadline-hint')\"\n />\n </div>\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n formControlName=\"requireDescription\"\n [label]=\"t('require-description')\"\n />\n <mt-toggle-field\n formControlName=\"allowFutureEntry\"\n [label]=\"t('allow-future-entry')\"\n />\n <mt-toggle-field\n formControlName=\"allowBackdatedEntry\"\n [label]=\"t('allow-backdated-entry')\"\n />\n <mt-toggle-field\n formControlName=\"autoLockOnApprove\"\n [label]=\"t('auto-lock-on-approve')\"\n />\n </div>\n </div>\n </mt-card>\n\n <mt-card class=\"block\">\n <div [formGroup]=\"globalWorkingHoursForm()\" class=\"space-y-5\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"working-hours-card-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"working-hours-card-description\") }}\n </p>\n </div>\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n class=\"md:col-span-2\"\n formControlName=\"timeZoneId\"\n [options]=\"timezoneOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('global-timezone')\"\n [placeholder]=\"t('global-timezone-placeholder')\"\n [filter]=\"true\"\n [required]=\"true\"\n />\n\n <mt-multi-select-field\n class=\"md:col-span-2\"\n formControlName=\"workingDays\"\n [options]=\"dayOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'dayNumber'\"\n [label]=\"t('working-days')\"\n [placeholder]=\"t('working-days-placeholder')\"\n [display]=\"'chip'\"\n [required]=\"true\"\n [showClear]=\"true\"\n />\n\n <mt-number-field\n class=\"md:col-span-2\"\n formControlName=\"hoursPerDay\"\n [label]=\"t('daily-working-hours')\"\n [placeholder]=\"t('daily-working-hours-placeholder')\"\n [min]=\"0\"\n [max]=\"24\"\n [required]=\"true\"\n />\n\n <mt-textarea-field\n class=\"md:col-span-2\"\n formControlName=\"holidaysCsv\"\n [label]=\"t('global-holidays')\"\n [placeholder]=\"t('global-holidays-placeholder')\"\n rows=\"3\"\n />\n </div>\n </div>\n </mt-card>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: MultiSelectField, selector: "mt-multi-select-field", inputs: ["field", "label", "placeholder", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "display", "required", "maxSelectedLabels", "group", "virtualScroll", "virtualScrollItemSize", "optionGroupLabel", "optionGroupChildren", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape"], outputs: ["onChange"] }, { kind: "component", type: NumberField, selector: "mt-number-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "pInputs", "format", "useGrouping", "maxFractionDigits", "min", "max", "required"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
214
+ }
215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettingsTimeSheetSection, decorators: [{
216
+ type: Component,
217
+ args: [{ selector: 'mt-system-settings-timesheet-section', imports: [
218
+ ReactiveFormsModule,
219
+ TranslocoDirective,
220
+ Card,
221
+ MultiSelectField,
222
+ NumberField,
223
+ SelectField,
224
+ TextareaField,
225
+ ToggleField,
226
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\n <div class=\"grid gap-6 xl:grid-cols-2 mb-6\">\n <mt-card class=\"block\">\n <div [formGroup]=\"policyForm()\" class=\"space-y-5\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"general-card-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"general-card-description\") }}\n </p>\n </div>\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n formControlName=\"frequency\"\n [options]=\"frequencyOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('frequency')\"\n [placeholder]=\"t('frequency-placeholder')\"\n [required]=\"true\"\n />\n\n <mt-number-field\n formControlName=\"maxDailyHours\"\n [label]=\"t('max-daily-hours')\"\n [placeholder]=\"t('max-daily-hours-placeholder')\"\n [min]=\"1\"\n [max]=\"24\"\n [required]=\"true\"\n [hint]=\"maxDailyBelowDaily() ? t('max-daily-below-daily') : ''\"\n />\n\n <mt-number-field\n formControlName=\"submissionDeadlineDays\"\n [label]=\"t('submission-deadline-days')\"\n [placeholder]=\"t('submission-deadline-days-placeholder')\"\n [min]=\"0\"\n [max]=\"90\"\n [required]=\"true\"\n [hint]=\"t('submission-deadline-hint')\"\n />\n </div>\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n formControlName=\"requireDescription\"\n [label]=\"t('require-description')\"\n />\n <mt-toggle-field\n formControlName=\"allowFutureEntry\"\n [label]=\"t('allow-future-entry')\"\n />\n <mt-toggle-field\n formControlName=\"allowBackdatedEntry\"\n [label]=\"t('allow-backdated-entry')\"\n />\n <mt-toggle-field\n formControlName=\"autoLockOnApprove\"\n [label]=\"t('auto-lock-on-approve')\"\n />\n </div>\n </div>\n </mt-card>\n\n <mt-card class=\"block\">\n <div [formGroup]=\"globalWorkingHoursForm()\" class=\"space-y-5\">\n <div class=\"space-y-1\">\n <h4 class=\"text-base font-semibold text-neutral-900\">\n {{ t(\"working-hours-card-title\") }}\n </h4>\n <p class=\"text-sm text-neutral-600\">\n {{ t(\"working-hours-card-description\") }}\n </p>\n </div>\n\n <div class=\"grid gap-4 md:grid-cols-2\">\n <mt-select-field\n class=\"md:col-span-2\"\n formControlName=\"timeZoneId\"\n [options]=\"timezoneOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'id'\"\n [label]=\"t('global-timezone')\"\n [placeholder]=\"t('global-timezone-placeholder')\"\n [filter]=\"true\"\n [required]=\"true\"\n />\n\n <mt-multi-select-field\n class=\"md:col-span-2\"\n formControlName=\"workingDays\"\n [options]=\"dayOptions()\"\n [optionLabel]=\"'name'\"\n [optionValue]=\"'dayNumber'\"\n [label]=\"t('working-days')\"\n [placeholder]=\"t('working-days-placeholder')\"\n [display]=\"'chip'\"\n [required]=\"true\"\n [showClear]=\"true\"\n />\n\n <mt-number-field\n class=\"md:col-span-2\"\n formControlName=\"hoursPerDay\"\n [label]=\"t('daily-working-hours')\"\n [placeholder]=\"t('daily-working-hours-placeholder')\"\n [min]=\"0\"\n [max]=\"24\"\n [required]=\"true\"\n />\n\n <mt-textarea-field\n class=\"md:col-span-2\"\n formControlName=\"holidaysCsv\"\n [label]=\"t('global-holidays')\"\n [placeholder]=\"t('global-holidays-placeholder')\"\n rows=\"3\"\n />\n </div>\n </div>\n </mt-card>\n </div>\n</ng-container>\n" }]
227
+ }], propDecorators: { policyForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "policyForm", required: true }] }], globalWorkingHoursForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalWorkingHoursForm", required: true }] }], dayOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayOptions", required: false }] }], frequencyOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "frequencyOptions", required: false }] }], timezoneOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "timezoneOptions", required: false }] }], maxDailyBelowDaily: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDailyBelowDaily", required: false }] }] } });
228
+
229
+ /** The settings record General Configuration fields live in. */
230
+ const GENERAL_CONFIGURATION_SETTING_KEY = 'Identity';
231
+ const FIXED_CURRENCIES = [
232
+ {
233
+ code: 'SAR',
234
+ englishLabel: 'Saudi Riyal (SAR)',
235
+ arabicLabel: 'ريال سعودي (SAR)',
236
+ unit: {
237
+ en: 'SAR',
238
+ ar: 'ريال سعودي',
239
+ },
240
+ },
241
+ {
242
+ code: 'OMR',
243
+ englishLabel: 'Omani Rial (OMR)',
244
+ arabicLabel: 'ريال عماني (OMR)',
245
+ unit: {
246
+ en: 'OMR',
247
+ ar: 'ريال عماني',
248
+ },
249
+ },
250
+ {
251
+ code: 'AED',
252
+ englishLabel: 'UAE Dirham (AED)',
253
+ arabicLabel: 'درهم إماراتي (AED)',
254
+ unit: {
255
+ en: 'AED',
256
+ ar: 'درهم إماراتي',
257
+ },
258
+ },
259
+ ];
260
+ /**
261
+ * Explore landing views, mirroring the app-client explore header switcher
262
+ * (same icons) so the configured default reads as the same choice.
263
+ */
264
+ const EXPLORE_DEFAULT_VIEW_OPTIONS = [
265
+ {
266
+ id: 'cards',
267
+ translationKey: 'explore-default-view-cards',
268
+ icon: 'layout.grid-01',
269
+ },
270
+ {
271
+ id: 'timeline',
272
+ translationKey: 'explore-default-view-timeline',
273
+ icon: 'time.clock',
274
+ },
275
+ ];
276
+ const DEFAULT_GENERAL_CONFIGURATION = {
277
+ defaultExploreView: 'cards',
278
+ showDesignSwitcher: false,
279
+ };
280
+ const DEFAULT_DATE_FORMAT_OPTIONS = [
281
+ {
282
+ key: 'MM/dd/yyyy',
283
+ sample: 'MM/dd/yyyy [01/20/2023]',
284
+ isSelected: true,
285
+ },
286
+ {
287
+ key: 'yyyy/MM/dd',
288
+ sample: 'yyyy/MM/dd [2023/01/20]',
289
+ isSelected: false,
290
+ },
291
+ {
292
+ key: 'dd-MM-yyyy',
293
+ sample: 'dd-MM-yyyy [20-01-2023]',
294
+ isSelected: false,
295
+ },
296
+ {
297
+ key: 'yyyy-MM-dd',
298
+ sample: 'yyyy-MM-dd [2023-01-20]',
299
+ isSelected: false,
300
+ },
301
+ ];
302
+ const DEFAULT_TIME_FORMAT_OPTIONS = [
303
+ {
304
+ key: 'hh:mm tt',
305
+ sample: 'hh:mm tt [03:00 PM]',
306
+ isSelected: true,
307
+ },
308
+ {
309
+ key: 'HH:mm',
310
+ sample: 'HH:mm [15:00]',
311
+ isSelected: false,
312
+ },
313
+ {
314
+ key: 'H:mm:ss',
315
+ sample: 'H:mm:ss [15:00:00]',
316
+ isSelected: false,
317
+ },
318
+ {
319
+ key: 'hh:mm:ss',
320
+ sample: 'hh:mm:ss [03:00:00]',
321
+ isSelected: false,
322
+ },
323
+ ];
324
+ const WEEKEND_DAY_DEFINITIONS = [
325
+ { dayNumber: 1, translationKey: 'monday' },
326
+ { dayNumber: 2, translationKey: 'tuesday' },
327
+ { dayNumber: 3, translationKey: 'wednesday' },
328
+ { dayNumber: 4, translationKey: 'thursday' },
329
+ { dayNumber: 5, translationKey: 'friday' },
330
+ { dayNumber: 6, translationKey: 'saturday' },
331
+ { dayNumber: 7, translationKey: 'sunday' },
332
+ ];
333
+ const DOTNET_DAY_DEFINITIONS = [
334
+ { dayNumber: 0, translationKey: 'sunday' },
335
+ { dayNumber: 1, translationKey: 'monday' },
336
+ { dayNumber: 2, translationKey: 'tuesday' },
337
+ { dayNumber: 3, translationKey: 'wednesday' },
338
+ { dayNumber: 4, translationKey: 'thursday' },
339
+ { dayNumber: 5, translationKey: 'friday' },
340
+ { dayNumber: 6, translationKey: 'saturday' },
341
+ ];
342
+ const TIMESHEET_FREQUENCY_OPTIONS = [
343
+ { id: 'Daily', translationKey: 'frequency-daily' },
344
+ { id: 'Weekly', translationKey: 'frequency-weekly' },
345
+ { id: 'BiWeekly', translationKey: 'frequency-biweekly' },
346
+ { id: 'Monthly', translationKey: 'frequency-monthly' },
347
+ ];
348
+ const SCHEDULE_ITEM_TYPE_OPTIONS = [
349
+ { itemType: 'Task', translationKey: 'schedule-item-task' },
350
+ { itemType: 'Milestone', translationKey: 'schedule-item-milestone' },
351
+ { itemType: 'Deliverable', translationKey: 'schedule-item-deliverable' },
352
+ ];
353
+ const RATE_SOURCE_OPTIONS = [
354
+ { id: 'UserHourlyRate', translationKey: 'rate-source-user-hourly-rate' },
355
+ { id: 'None', translationKey: 'rate-source-none' },
356
+ ];
357
+
358
+ const SYSTEM_SETTINGS_STORAGE_KEY = 'systemSettings.settings';
359
+ function serializeSystemSettings(settings) {
360
+ return JSON.stringify(settings);
361
+ }
362
+ /**
363
+ * Persists the canonical settings array consumed by host-level formatters.
364
+ * Storage failures must not turn a successful API request into a UI failure.
365
+ */
366
+ function synchronizeSystemSettingsStorage(storage, settings) {
367
+ if (!storage) {
368
+ return false;
369
+ }
370
+ try {
371
+ storage.setItem(SYSTEM_SETTINGS_STORAGE_KEY, serializeSystemSettings(settings));
372
+ return true;
373
+ }
374
+ catch {
375
+ return false;
376
+ }
377
+ }
378
+ function resolveBrowserLocalStorage() {
379
+ try {
380
+ return globalThis.localStorage ?? null;
381
+ }
382
+ catch {
383
+ return null;
384
+ }
385
+ }
386
+
387
+ class SystemSettings {
388
+ http = inject(HttpClient);
389
+ router = inject(Router);
390
+ transloco = inject(TranslocoService);
391
+ modal = inject(ModalService);
392
+ toast = inject(ToastService);
393
+ apiBasePath = input('settings', ...(ngDevMode ? [{ debugName: "apiBasePath" }] : /* istanbul ignore next */ []));
394
+ backRoute = input('/control-panel/product-settings', ...(ngDevMode ? [{ debugName: "backRoute" }] : /* istanbul ignore next */ []));
395
+ formatSchema = input(null, ...(ngDevMode ? [{ debugName: "formatSchema" }] : /* istanbul ignore next */ []));
396
+ breadcrumbItems = [
397
+ {
398
+ label: '',
399
+ icon: 'general.home-line',
400
+ routerLink: '/control-panel/workspaces',
401
+ },
402
+ {
403
+ label: this.transloco.translate('product-settings.product-settings'),
404
+ routerLink: '/control-panel/product-settings',
405
+ },
406
+ {
407
+ label: this.transloco.translate('system-settings.page-title'),
408
+ },
409
+ ];
410
+ form = new FormGroup({
411
+ generalConfiguration: new FormGroup({
412
+ defaultExploreView: new FormControl('cards', {
413
+ nonNullable: true,
414
+ validators: [Validators.required],
415
+ }),
416
+ showDesignSwitcher: new FormControl(false, {
417
+ nonNullable: true,
418
+ }),
419
+ }),
420
+ timezone: new FormGroup({
421
+ regionId: new FormControl(null, Validators.required),
422
+ dateFormat: new FormControl(null, Validators.required),
423
+ timeFormat: new FormControl(null, Validators.required),
424
+ }),
425
+ currency: new FormGroup({
426
+ code: new FormControl(null, Validators.required),
427
+ format: new FormControl(null, Validators.required),
428
+ }),
429
+ weekend: new FormGroup({
430
+ days: new FormControl([], { nonNullable: true }),
431
+ }),
432
+ timeSheetPolicy: new FormGroup({
433
+ frequency: new FormControl('Weekly', Validators.required),
434
+ maxDailyHours: new FormControl(24, [
435
+ Validators.required,
436
+ Validators.min(1),
437
+ Validators.max(24),
438
+ ]),
439
+ requireDescription: new FormControl(false, { nonNullable: true }),
440
+ allowFutureEntry: new FormControl(false, { nonNullable: true }),
441
+ allowBackdatedEntry: new FormControl(true, { nonNullable: true }),
442
+ autoLockOnApprove: new FormControl(true, { nonNullable: true }),
443
+ submissionDeadlineDays: new FormControl(3, [
444
+ Validators.required,
445
+ Validators.min(0),
446
+ Validators.max(90),
447
+ ]),
448
+ }),
449
+ globalWorkingHours: new FormGroup({
450
+ timeZoneId: new FormControl(null, Validators.required),
451
+ workingDays: new FormControl([0, 1, 2, 3, 4], {
452
+ nonNullable: true,
453
+ validators: [Validators.required],
454
+ }),
455
+ hoursPerDay: new FormControl(8, [
456
+ Validators.required,
457
+ Validators.min(0),
458
+ Validators.max(24),
459
+ ]),
460
+ holidaysCsv: new FormControl('', { nonNullable: true }),
461
+ }),
462
+ }, maxDailyHoursGteHoursPerDay);
463
+ settings = signal(null, ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
464
+ settingsLoading = signal(false, ...(ngDevMode ? [{ debugName: "settingsLoading" }] : /* istanbul ignore next */ []));
465
+ settingsError = signal(null, ...(ngDevMode ? [{ debugName: "settingsError" }] : /* istanbul ignore next */ []));
466
+ settingsLastUpdated = signal(null, ...(ngDevMode ? [{ debugName: "settingsLastUpdated" }] : /* istanbul ignore next */ []));
467
+ updating = signal(false, ...(ngDevMode ? [{ debugName: "updating" }] : /* istanbul ignore next */ []));
468
+ updateError = signal(null, ...(ngDevMode ? [{ debugName: "updateError" }] : /* istanbul ignore next */ []));
469
+ timezones = signal([], ...(ngDevMode ? [{ debugName: "timezones" }] : /* istanbul ignore next */ []));
470
+ timezonesLoading = signal(false, ...(ngDevMode ? [{ debugName: "timezonesLoading" }] : /* istanbul ignore next */ []));
471
+ timezonesError = signal(null, ...(ngDevMode ? [{ debugName: "timezonesError" }] : /* istanbul ignore next */ []));
472
+ holidayItems = signal([], ...(ngDevMode ? [{ debugName: "holidayItems" }] : /* istanbul ignore next */ []));
473
+ initialSnapshot = signal(null, ...(ngDevMode ? [{ debugName: "initialSnapshot" }] : /* istanbul ignore next */ []));
474
+ hydrationVersion = signal(null, ...(ngDevMode ? [{ debugName: "hydrationVersion" }] : /* istanbul ignore next */ []));
475
+ activeLang = toSignal(this.transloco.langChanges$, {
476
+ initialValue: this.transloco.getActiveLang(),
477
+ });
478
+ formStatus = toSignal(this.form.statusChanges, {
479
+ initialValue: this.form.status,
480
+ });
481
+ selectedDateFormat = toSignal(this.form.controls.timezone.controls.dateFormat.valueChanges, {
482
+ initialValue: this.form.controls.timezone.controls.dateFormat.value,
483
+ });
484
+ selectedTimeFormat = toSignal(this.form.controls.timezone.controls.timeFormat.valueChanges, {
485
+ initialValue: this.form.controls.timezone.controls.timeFormat.value,
486
+ });
487
+ selectedCurrencyCode = toSignal(this.form.controls.currency.controls.code.valueChanges, {
488
+ initialValue: this.form.controls.currency.controls.code.value,
489
+ });
490
+ formValue = toSignal(this.form.valueChanges, {
491
+ initialValue: this.form.getRawValue(),
492
+ });
493
+ /**
494
+ * The `Identity` record. It is shared with `@masterteam/brand-display`, which
495
+ * owns every other field in it — we only read/write the General Configuration
496
+ * slice and must preserve the rest verbatim.
497
+ */
498
+ generalConfigurationSetting = computed(() => this.findSettingByKey(GENERAL_CONFIGURATION_SETTING_KEY), ...(ngDevMode ? [{ debugName: "generalConfigurationSetting" }] : /* istanbul ignore next */ []));
499
+ holidaysSetting = computed(() => this.findSettingByKey('Holidays'), ...(ngDevMode ? [{ debugName: "holidaysSetting" }] : /* istanbul ignore next */ []));
500
+ timezoneSetting = computed(() => this.findSettingByKey('Timezone'), ...(ngDevMode ? [{ debugName: "timezoneSetting" }] : /* istanbul ignore next */ []));
501
+ currencySetting = computed(() => this.findSettingByKey('Currency'), ...(ngDevMode ? [{ debugName: "currencySetting" }] : /* istanbul ignore next */ []));
502
+ weekendSetting = computed(() => this.findSettingByKey('Weekend'), ...(ngDevMode ? [{ debugName: "weekendSetting" }] : /* istanbul ignore next */ []));
503
+ timeSheetPolicySetting = computed(() => this.findSettingByKey('TimeSheetPolicy'), ...(ngDevMode ? [{ debugName: "timeSheetPolicySetting" }] : /* istanbul ignore next */ []));
504
+ globalWorkingHoursSetting = computed(() => this.findSettingByKey('GlobalWorkingHours'), ...(ngDevMode ? [{ debugName: "globalWorkingHoursSetting" }] : /* istanbul ignore next */ []));
505
+ resolvedFormatSchema = computed(() => {
506
+ const schema = this.formatSchema();
507
+ return {
508
+ dateFormats: schema?.dateFormats ?? DEFAULT_DATE_FORMAT_OPTIONS,
509
+ timeFormats: schema?.timeFormats ?? DEFAULT_TIME_FORMAT_OPTIONS,
510
+ };
511
+ }, ...(ngDevMode ? [{ debugName: "resolvedFormatSchema" }] : /* istanbul ignore next */ []));
512
+ timezoneOptions = computed(() => this.timezones().map((timezone) => ({
513
+ ...timezone,
514
+ name: timezone.name ?? timezone.displayName,
515
+ })), ...(ngDevMode ? [{ debugName: "timezoneOptions" }] : /* istanbul ignore next */ []));
516
+ dateFormatOptions = computed(() => this.mapFormatOptions(this.resolvedFormatSchema().dateFormats, this.selectedDateFormat()), ...(ngDevMode ? [{ debugName: "dateFormatOptions" }] : /* istanbul ignore next */ []));
517
+ timeFormatOptions = computed(() => this.mapFormatOptions(this.resolvedFormatSchema().timeFormats, this.selectedTimeFormat()), ...(ngDevMode ? [{ debugName: "timeFormatOptions" }] : /* istanbul ignore next */ []));
518
+ currencyOptions = computed(() => {
519
+ const lang = this.activeLang();
520
+ return FIXED_CURRENCIES.map((currency) => ({
521
+ id: currency.code,
522
+ name: lang === 'ar' ? currency.arabicLabel : currency.englishLabel,
523
+ }));
524
+ }, ...(ngDevMode ? [{ debugName: "currencyOptions" }] : /* istanbul ignore next */ []));
525
+ currencyFormatOptions = computed(() => {
526
+ const currency = FIXED_CURRENCIES.find((item) => item.code === this.selectedCurrencyCode());
527
+ return this.buildCurrencyFormatOptions(currency);
528
+ }, ...(ngDevMode ? [{ debugName: "currencyFormatOptions" }] : /* istanbul ignore next */ []));
529
+ weekendOptions = computed(() => {
530
+ this.activeLang();
531
+ return WEEKEND_DAY_DEFINITIONS.map((day) => ({
532
+ id: `${day.dayNumber}`,
533
+ dayNumber: day.dayNumber,
534
+ name: this.transloco.translate(`system-settings.${day.translationKey}`),
535
+ }));
536
+ }, ...(ngDevMode ? [{ debugName: "weekendOptions" }] : /* istanbul ignore next */ []));
537
+ dotnetDayOptions = computed(() => {
538
+ this.activeLang();
539
+ return DOTNET_DAY_DEFINITIONS.map((day) => ({
540
+ id: `${day.dayNumber}`,
541
+ dayNumber: day.dayNumber,
542
+ name: this.transloco.translate(`system-settings.${day.translationKey}`),
543
+ }));
544
+ }, ...(ngDevMode ? [{ debugName: "dotnetDayOptions" }] : /* istanbul ignore next */ []));
545
+ exploreDefaultViewOptions = computed(() => {
546
+ this.activeLang();
547
+ return EXPLORE_DEFAULT_VIEW_OPTIONS.map((option) => ({
548
+ id: option.id,
549
+ name: this.transloco.translate(`system-settings.${option.translationKey}`),
550
+ icon: option.icon,
551
+ }));
552
+ }, ...(ngDevMode ? [{ debugName: "exploreDefaultViewOptions" }] : /* istanbul ignore next */ []));
553
+ frequencyOptions = computed(() => {
554
+ this.activeLang();
555
+ return TIMESHEET_FREQUENCY_OPTIONS.map((item) => ({
556
+ id: item.id,
557
+ name: this.transloco.translate(`system-settings.${item.translationKey}`),
558
+ }));
559
+ }, ...(ngDevMode ? [{ debugName: "frequencyOptions" }] : /* istanbul ignore next */ []));
560
+ isReady = computed(() => this.hydrationVersion() !== null && this.initialSnapshot() !== null, ...(ngDevMode ? [{ debugName: "isReady" }] : /* istanbul ignore next */ []));
561
+ isLoading = computed(() => this.settingsLoading() || this.timezonesLoading() || !this.isReady(), ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
562
+ hasChanges = computed(() => {
563
+ this.formValue();
564
+ this.holidayItems();
565
+ const initial = this.initialSnapshot();
566
+ if (!initial) {
567
+ return false;
568
+ }
569
+ const current = this.buildCurrentSnapshot();
570
+ if (!current) {
571
+ return false;
572
+ }
573
+ return (!this.areEqual(initial.generalConfiguration, current.generalConfiguration) ||
574
+ !this.areEqual(initial.holidays, current.holidays) ||
575
+ !this.areEqual(initial.timezone, current.timezone) ||
576
+ !this.areEqual(initial.currency, current.currency) ||
577
+ !this.areEqual(initial.weekend, current.weekend) ||
578
+ !this.areEqual(initial.timeSheetPolicy, current.timeSheetPolicy) ||
579
+ !this.areEqual(initial.globalWorkingHours, current.globalWorkingHours));
580
+ }, ...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
581
+ canSaveSettings = computed(() => !this.isLoading() &&
582
+ !this.updating() &&
583
+ this.formStatus() === 'VALID' &&
584
+ this.hasChanges(), ...(ngDevMode ? [{ debugName: "canSaveSettings" }] : /* istanbul ignore next */ []));
585
+ holidayTableRows = computed(() => this.holidayItems().map((holiday, index) => ({
586
+ rowKey: `${index}`,
587
+ rowIndex: index,
588
+ label: holiday.label,
589
+ from: formatDateOnly(holiday.from),
590
+ to: formatDateOnly(holiday.to),
591
+ })), ...(ngDevMode ? [{ debugName: "holidayTableRows" }] : /* istanbul ignore next */ []));
592
+ constructor() {
593
+ effect(() => {
594
+ const version = [
595
+ this.settingsLastUpdated() ?? 'none',
596
+ this.settingsError() ?? 'none',
597
+ this.timezonesError() ?? 'none',
598
+ String(this.timezones().length),
599
+ JSON.stringify(this.resolvedFormatSchema()),
600
+ ].join('::');
601
+ if (this.settingsLoading() || this.timezonesLoading()) {
602
+ return;
603
+ }
604
+ if (version === this.hydrationVersion()) {
605
+ return;
606
+ }
607
+ this.hydrateForm(this.generalConfigurationSetting() ??
608
+ this.createFallbackSetting(GENERAL_CONFIGURATION_SETTING_KEY), this.holidaysSetting() ?? this.createFallbackSetting('Holidays'), this.timezoneSetting() ?? this.createFallbackSetting('Timezone'), this.currencySetting() ?? this.createFallbackSetting('Currency'), this.weekendSetting() ?? this.createFallbackSetting('Weekend'), this.timeSheetPolicySetting() ??
609
+ this.createFallbackSetting('TimeSheetPolicy'), this.globalWorkingHoursSetting() ??
610
+ this.createFallbackSetting('GlobalWorkingHours'));
611
+ this.hydrationVersion.set(version);
612
+ });
613
+ }
614
+ ngOnInit() {
615
+ this.loadSettings();
616
+ this.loadTimezones();
617
+ }
618
+ goBack() {
619
+ const backRoute = this.backRoute();
620
+ if (!backRoute) {
621
+ return;
622
+ }
623
+ this.router.navigateByUrl(backRoute);
624
+ }
625
+ openAddHoliday() {
626
+ const modalRef = this.modal.openModal(SystemSettingsHolidayEditor, 'dialog', {
627
+ header: this.transloco.translate('system-settings.add-holiday'),
628
+ width: '42rem',
629
+ appendTo: 'body',
630
+ dismissableMask: true,
631
+ dismissible: true,
632
+ inputValues: {
633
+ holiday: null,
634
+ isEditMode: false,
635
+ },
636
+ });
637
+ modalRef.onClose.subscribe((result) => {
638
+ if (!result) {
639
+ return;
640
+ }
641
+ this.holidayItems.update((items) => [...items, result]);
642
+ });
643
+ }
644
+ editHoliday(index) {
645
+ const holiday = this.holidayItems()[index];
646
+ if (!holiday) {
647
+ return;
648
+ }
649
+ const modalRef = this.modal.openModal(SystemSettingsHolidayEditor, 'drawer', {
650
+ header: this.transloco.translate('system-settings.edit-holiday'),
651
+ styleClass: '!w-[30rem] !absolute !shadow-none',
652
+ position: 'end',
653
+ appendTo: 'page-content',
654
+ dismissible: true,
655
+ inputValues: {
656
+ holiday,
657
+ isEditMode: true,
658
+ },
659
+ });
660
+ modalRef.onClose.subscribe((result) => {
661
+ if (!result) {
662
+ return;
663
+ }
664
+ this.holidayItems.update((items) => items.map((item, itemIndex) => (itemIndex === index ? result : item)));
665
+ });
666
+ }
667
+ deleteHoliday(index) {
668
+ this.holidayItems.update((items) => items.filter((_, itemIndex) => itemIndex !== index));
669
+ }
670
+ onSave() {
671
+ this.form.markAllAsTouched();
672
+ const nextSnapshot = this.buildCurrentSnapshot();
673
+ const initialSnapshot = this.initialSnapshot();
674
+ if (!nextSnapshot || !initialSnapshot) {
675
+ return;
676
+ }
677
+ const values = [];
678
+ if (!this.areEqual(initialSnapshot.generalConfiguration, nextSnapshot.generalConfiguration)) {
679
+ // `Identity` is shared with brand-display and is saved by full
680
+ // replacement, so merge our slice onto the record as last loaded instead
681
+ // of sending it alone — sending only our fields would wipe the branding.
682
+ values.push({
683
+ key: GENERAL_CONFIGURATION_SETTING_KEY,
684
+ value: {
685
+ ...this.getIdentityRecord(),
686
+ ...nextSnapshot.generalConfiguration,
687
+ },
688
+ });
689
+ }
690
+ if (!this.areEqual(initialSnapshot.holidays, nextSnapshot.holidays)) {
691
+ values.push({
692
+ key: 'Holidays',
693
+ value: this.serializeHolidaysValue(nextSnapshot.holidays),
694
+ });
695
+ }
696
+ if (!this.areEqual(initialSnapshot.timezone, nextSnapshot.timezone)) {
697
+ values.push({
698
+ key: 'Timezone',
699
+ value: nextSnapshot.timezone,
700
+ });
701
+ }
702
+ if (!this.areEqual(initialSnapshot.currency, nextSnapshot.currency)) {
703
+ values.push({
704
+ key: 'Currency',
705
+ value: nextSnapshot.currency,
706
+ });
707
+ }
708
+ if (!this.areEqual(initialSnapshot.weekend, nextSnapshot.weekend)) {
709
+ values.push({
710
+ key: 'Weekend',
711
+ value: nextSnapshot.weekend,
712
+ });
713
+ }
714
+ if (!this.areEqual(initialSnapshot.timeSheetPolicy, nextSnapshot.timeSheetPolicy)) {
715
+ values.push({
716
+ key: 'TimeSheetPolicy',
717
+ value: nextSnapshot.timeSheetPolicy,
718
+ });
719
+ }
720
+ if (!this.areEqual(initialSnapshot.globalWorkingHours, nextSnapshot.globalWorkingHours)) {
721
+ values.push({
722
+ key: 'GlobalWorkingHours',
723
+ value: nextSnapshot.globalWorkingHours,
724
+ });
725
+ }
726
+ if (!values.length) {
727
+ this.toast.info(this.transloco.translate('system-settings.no-changes-message'));
728
+ return;
729
+ }
730
+ this.updateSettings(values);
731
+ }
732
+ loadSettings() {
733
+ this.settingsLoading.set(true);
734
+ this.settingsError.set(null);
735
+ this.http
736
+ .get(this.buildEndpoint('getsystemsettings'))
737
+ .subscribe({
738
+ next: (response) => {
739
+ const settings = response.data ?? [];
740
+ this.settings.set(settings);
741
+ synchronizeSystemSettingsStorage(resolveBrowserLocalStorage(), settings);
742
+ this.settingsLoading.set(false);
743
+ this.settingsLastUpdated.set(new Date().toISOString());
744
+ },
745
+ error: (error) => {
746
+ this.settingsLoading.set(false);
747
+ this.settingsError.set(error?.message ?? 'Failed to load system settings');
748
+ this.settingsLastUpdated.set(new Date().toISOString());
749
+ },
750
+ });
751
+ }
752
+ loadTimezones() {
753
+ this.timezonesLoading.set(true);
754
+ this.timezonesError.set(null);
755
+ this.http
756
+ .get(this.buildEndpoint('timezones'))
757
+ .subscribe({
758
+ next: (response) => {
759
+ this.timezones.set(response.data ?? []);
760
+ this.timezonesLoading.set(false);
761
+ },
762
+ error: (error) => {
763
+ this.timezonesLoading.set(false);
764
+ this.timezonesError.set(error?.message ?? 'Failed to load timezone options');
765
+ },
766
+ });
767
+ }
768
+ updateSettings(values) {
769
+ this.updating.set(true);
770
+ this.updateError.set(null);
771
+ this.http
772
+ .put(this.buildEndpoint('updatesystemsettings'), {
773
+ values,
774
+ })
775
+ .subscribe({
776
+ next: () => {
777
+ const nextSettings = this.applyUpdatedSettings(this.settings() ?? [], values);
778
+ this.settings.set(nextSettings);
779
+ synchronizeSystemSettingsStorage(resolveBrowserLocalStorage(), nextSettings);
780
+ this.updating.set(false);
781
+ this.updateError.set(null);
782
+ this.settingsLastUpdated.set(new Date().toISOString());
783
+ },
784
+ error: (error) => {
785
+ this.updating.set(false);
786
+ this.updateError.set(error?.message ?? 'Failed to update system settings');
787
+ },
788
+ });
789
+ }
790
+ hydrateForm(generalConfigurationSetting, holidaysSetting, timezoneSetting, currencySetting, weekendSetting, timeSheetPolicySetting, globalWorkingHoursSetting) {
791
+ const generalConfiguration = this.normalizeGeneralConfigurationSnapshot(this.extractGeneralConfigurationValue(generalConfigurationSetting));
792
+ const holidays = this.extractHolidaysValue(holidaysSetting);
793
+ const timezone = this.normalizeTimezoneSnapshot(this.extractTimezoneValue(timezoneSetting));
794
+ const currency = this.normalizeCurrencySnapshot(this.extractCurrencyValue(currencySetting));
795
+ const weekend = this.normalizeWeekendSnapshot(this.extractWeekendValue(weekendSetting));
796
+ const globalWorkingHours = this.normalizeGlobalWorkingHoursSnapshot(this.extractGlobalWorkingHoursValue(globalWorkingHoursSetting));
797
+ const timeSheetPolicy = this.normalizeTimeSheetPolicySnapshot(this.extractTimeSheetPolicyValue(timeSheetPolicySetting), globalWorkingHours.hoursPerDay);
798
+ this.holidayItems.set(holidays.data);
799
+ this.form.controls.generalConfiguration.setValue({
800
+ defaultExploreView: generalConfiguration.defaultExploreView,
801
+ showDesignSwitcher: generalConfiguration.showDesignSwitcher,
802
+ });
803
+ this.form.controls.timezone.setValue({
804
+ regionId: timezone.region?.id ?? null,
805
+ dateFormat: timezone.dateFormat,
806
+ timeFormat: timezone.timeFormat,
807
+ });
808
+ this.form.controls.currency.setValue({
809
+ code: this.resolveCurrencyCode(currency),
810
+ format: currency.format,
811
+ });
812
+ this.form.controls.weekend.setValue({
813
+ days: weekend.days,
814
+ });
815
+ this.form.controls.timeSheetPolicy.setValue({
816
+ frequency: timeSheetPolicy.frequency,
817
+ maxDailyHours: timeSheetPolicy.maxDailyHours,
818
+ requireDescription: timeSheetPolicy.requireDescription,
819
+ allowFutureEntry: timeSheetPolicy.allowFutureEntry,
820
+ allowBackdatedEntry: timeSheetPolicy.allowBackdatedEntry,
821
+ autoLockOnApprove: timeSheetPolicy.autoLockOnApprove,
822
+ submissionDeadlineDays: timeSheetPolicy.submissionDeadlineDays,
823
+ });
824
+ this.form.controls.globalWorkingHours.setValue({
825
+ timeZoneId: globalWorkingHours.timeZoneId,
826
+ workingDays: globalWorkingHours.workingDays,
827
+ hoursPerDay: globalWorkingHours.hoursPerDay,
828
+ holidaysCsv: globalWorkingHours.holidays.join(', '),
829
+ });
830
+ this.form.markAsPristine();
831
+ this.form.markAsUntouched();
832
+ this.initialSnapshot.set({
833
+ generalConfiguration,
834
+ holidays,
835
+ timezone,
836
+ currency,
837
+ weekend,
838
+ timeSheetPolicy,
839
+ globalWorkingHours,
840
+ });
841
+ }
842
+ buildCurrentSnapshot() {
843
+ const timezone = this.buildTimezoneValue();
844
+ const currency = this.buildCurrencyValue();
845
+ if (!timezone || !currency || this.form.invalid) {
846
+ return null;
847
+ }
848
+ return {
849
+ generalConfiguration: this.buildGeneralConfigurationValue(),
850
+ holidays: {
851
+ data: this.holidayItems().map((item) => ({
852
+ label: item.label.trim(),
853
+ from: item.from,
854
+ to: item.to,
855
+ })),
856
+ },
857
+ timezone,
858
+ currency,
859
+ weekend: this.buildWeekendValue(),
860
+ timeSheetPolicy: this.buildTimeSheetPolicyValue(),
861
+ globalWorkingHours: this.buildGlobalWorkingHoursValue(),
862
+ };
863
+ }
864
+ buildTimezoneValue() {
865
+ const timezoneForm = this.form.controls.timezone.getRawValue();
866
+ if (!timezoneForm.regionId ||
867
+ !timezoneForm.dateFormat ||
868
+ !timezoneForm.timeFormat) {
869
+ return null;
870
+ }
871
+ const region = this.timezones().find((item) => item.id === timezoneForm.regionId);
872
+ if (!region) {
873
+ return null;
874
+ }
875
+ return {
876
+ region: {
877
+ ...region,
878
+ name: region.name ?? region.displayName,
879
+ },
880
+ dateFormat: timezoneForm.dateFormat,
881
+ timeFormat: timezoneForm.timeFormat,
882
+ };
883
+ }
884
+ buildCurrencyValue() {
885
+ const currencyForm = this.form.controls.currency.getRawValue();
886
+ if (!currencyForm.code || !currencyForm.format) {
887
+ return null;
888
+ }
889
+ const currency = FIXED_CURRENCIES.find((item) => item.code === currencyForm.code);
890
+ if (!currency) {
891
+ return null;
892
+ }
893
+ return {
894
+ name: currency.code,
895
+ unit: currency.unit,
896
+ format: currencyForm.format,
897
+ };
898
+ }
899
+ /**
900
+ * The `Identity` value exactly as last returned by the API (or written by a
901
+ * previous save in this session), used as the base every save merges onto so
902
+ * branding fields this page does not model survive round-trips.
903
+ */
904
+ getIdentityRecord() {
905
+ const value = this.generalConfigurationSetting()?.value;
906
+ return isRecord(value) ? value : {};
907
+ }
908
+ buildGeneralConfigurationValue() {
909
+ return {
910
+ defaultExploreView: normalizeExploreDefaultView(this.form.controls.generalConfiguration.controls.defaultExploreView
911
+ .value),
912
+ showDesignSwitcher: this.form.controls.generalConfiguration.controls.showDesignSwitcher
913
+ .value === true,
914
+ };
915
+ }
916
+ buildWeekendValue() {
917
+ return {
918
+ days: normalizeWeekendDays(this.form.controls.weekend.controls.days.value),
919
+ };
920
+ }
921
+ buildTimeSheetPolicyValue() {
922
+ const policyForm = this.form.controls.timeSheetPolicy.getRawValue();
923
+ const hoursPerDay = clampNumber(this.form.controls.globalWorkingHours.controls.hoursPerDay.value ?? 8, 0, 24);
924
+ return {
925
+ frequency: normalizeFrequency(policyForm.frequency),
926
+ maxDailyHours: Math.max(hoursPerDay, clampNumber(policyForm.maxDailyHours ?? 24, 1, 24)),
927
+ requireDescription: policyForm.requireDescription,
928
+ allowFutureEntry: policyForm.allowFutureEntry,
929
+ allowBackdatedEntry: policyForm.allowBackdatedEntry,
930
+ autoLockOnApprove: policyForm.autoLockOnApprove,
931
+ submissionDeadlineDays: clampNumber(policyForm.submissionDeadlineDays ?? 0, 0, 90),
932
+ };
933
+ }
934
+ buildGlobalWorkingHoursValue() {
935
+ const globalForm = this.form.controls.globalWorkingHours.getRawValue();
936
+ return {
937
+ timeZoneId: globalForm.timeZoneId ?? 'UTC',
938
+ workingDays: normalizeDotnetDays(globalForm.workingDays),
939
+ hoursPerDay: clampNumber(globalForm.hoursPerDay ?? 8, 0, 24),
940
+ holidays: parseCsv(globalForm.holidaysCsv),
941
+ };
942
+ }
943
+ findSettingByKey(key) {
944
+ const normalizedKey = key.trim().toLowerCase();
945
+ return (this.settings()?.find((setting) => setting.key.trim().toLowerCase() === normalizedKey) ?? null);
946
+ }
947
+ extractGeneralConfigurationValue(setting) {
948
+ if (!isRecord(setting.value)) {
949
+ return { ...DEFAULT_GENERAL_CONFIGURATION };
950
+ }
951
+ return {
952
+ defaultExploreView: normalizeExploreDefaultView(setting.value['defaultExploreView'] ??
953
+ setting.value['DefaultExploreView']),
954
+ showDesignSwitcher: normalizeShowDesignSwitcher(setting.value['showDesignSwitcher'] ??
955
+ setting.value['ShowDesignSwitcher']),
956
+ };
957
+ }
958
+ extractHolidaysValue(setting) {
959
+ if (!isRecord(setting.value)) {
960
+ return { data: [] };
961
+ }
962
+ const rawData = setting.value['data'];
963
+ if (!Array.isArray(rawData)) {
964
+ return { data: [] };
965
+ }
966
+ return {
967
+ data: rawData
968
+ .map((item) => this.normalizeHolidayItem(item))
969
+ .filter((item) => item !== null),
970
+ };
971
+ }
972
+ extractTimezoneValue(setting) {
973
+ if (!isRecord(setting.value)) {
974
+ return {
975
+ region: null,
976
+ dateFormat: null,
977
+ timeFormat: null,
978
+ };
979
+ }
980
+ return {
981
+ region: this.normalizeTimezoneRegion(setting.value['region'] ?? setting.value['Region']),
982
+ dateFormat: getNullableString(setting.value['dateFormat'] ??
983
+ setting.value['DateFormat'] ??
984
+ setting.value['dateformat']),
985
+ timeFormat: getNullableString(setting.value['timeFormat'] ??
986
+ setting.value['TimeFormat'] ??
987
+ setting.value['timeformat']),
988
+ };
989
+ }
990
+ extractCurrencyValue(setting) {
991
+ if (!isRecord(setting.value)) {
992
+ return this.getDefaultCurrencyValue();
993
+ }
994
+ return {
995
+ name: getString(setting.value['name']) ?? FIXED_CURRENCIES[0].code,
996
+ unit: this.normalizeCurrencyUnit(setting.value['unit']),
997
+ format: getNullableString(setting.value['format']),
998
+ };
999
+ }
1000
+ extractWeekendValue(setting) {
1001
+ if (!isRecord(setting.value)) {
1002
+ return { days: [] };
1003
+ }
1004
+ return {
1005
+ days: normalizeWeekendDays(setting.value['days'] ?? setting.value['Days']),
1006
+ };
1007
+ }
1008
+ extractTimeSheetPolicyValue(setting) {
1009
+ if (!isRecord(setting.value)) {
1010
+ return this.getDefaultTimeSheetPolicyValue();
1011
+ }
1012
+ const value = setting.value;
1013
+ const defaults = this.getDefaultTimeSheetPolicyValue();
1014
+ return {
1015
+ frequency: normalizeFrequency(value['frequency'] ?? value['Frequency']),
1016
+ maxDailyHours: getNumber(value['maxDailyHours'] ?? value['MaxDailyHours']) ??
1017
+ defaults.maxDailyHours,
1018
+ requireDescription: getBoolean(value['requireDescription'] ?? value['RequireDescription']) ?? defaults.requireDescription,
1019
+ allowFutureEntry: getBoolean(value['allowFutureEntry'] ?? value['AllowFutureEntry']) ??
1020
+ defaults.allowFutureEntry,
1021
+ allowBackdatedEntry: getBoolean(value['allowBackdatedEntry'] ?? value['AllowBackdatedEntry']) ?? defaults.allowBackdatedEntry,
1022
+ autoLockOnApprove: getBoolean(value['autoLockOnApprove'] ?? value['AutoLockOnApprove']) ??
1023
+ defaults.autoLockOnApprove,
1024
+ submissionDeadlineDays: getNumber(value['submissionDeadlineDays'] ?? value['SubmissionDeadlineDays']) ?? defaults.submissionDeadlineDays,
1025
+ };
1026
+ }
1027
+ extractGlobalWorkingHoursValue(setting) {
1028
+ if (!isRecord(setting.value)) {
1029
+ return this.getDefaultGlobalWorkingHoursValue();
1030
+ }
1031
+ const value = setting.value;
1032
+ const defaults = this.getDefaultGlobalWorkingHoursValue();
1033
+ return {
1034
+ timeZoneId: getString(value['timeZoneId'] ?? value['TimeZoneId']) ??
1035
+ defaults.timeZoneId,
1036
+ workingDays: getNumberArray(value['workingDays'] ?? value['WorkingDays']) ??
1037
+ defaults.workingDays,
1038
+ hoursPerDay: getNumber(value['hoursPerDay'] ?? value['HoursPerDay']) ??
1039
+ defaults.hoursPerDay,
1040
+ holidays: getStringArray(value['holidays'] ?? value['Holidays']) ??
1041
+ defaults.holidays,
1042
+ };
1043
+ }
1044
+ normalizeHolidayItem(value) {
1045
+ if (!isRecord(value)) {
1046
+ return null;
1047
+ }
1048
+ const label = getString(value['label'] ?? value['name']);
1049
+ const from = normalizeDateInput(value['from']);
1050
+ const to = normalizeDateInput(value['to']);
1051
+ if (!label || !from || !to) {
1052
+ return null;
1053
+ }
1054
+ return { label, from, to };
1055
+ }
1056
+ normalizeTimezoneRegion(value) {
1057
+ if (typeof value === 'string' && value.trim()) {
1058
+ const normalizedValue = value.trim().toLowerCase();
1059
+ const region = this.timezones().find((item) => item.id.trim().toLowerCase() === normalizedValue) ?? null;
1060
+ return region
1061
+ ? {
1062
+ ...region,
1063
+ name: region.name ?? region.displayName,
1064
+ }
1065
+ : null;
1066
+ }
1067
+ if (!isRecord(value)) {
1068
+ return null;
1069
+ }
1070
+ const id = getString(value['id'] ?? value['Id']);
1071
+ if (!id) {
1072
+ return null;
1073
+ }
1074
+ const matchedRegion = this.timezones().find((item) => item.id.trim().toLowerCase() === id.trim().toLowerCase());
1075
+ if (matchedRegion) {
1076
+ return {
1077
+ ...matchedRegion,
1078
+ name: getString(value['name'] ?? value['Name']) ??
1079
+ matchedRegion.name ??
1080
+ matchedRegion.displayName,
1081
+ };
1082
+ }
1083
+ const standardName = getString(value['standardName'] ?? value['StandardName']) ?? id;
1084
+ const displayName = getString(value['displayName'] ?? value['DisplayName']) ?? id;
1085
+ return {
1086
+ id,
1087
+ baseUtcOffset: getString(value['baseUtcOffset'] ?? value['BaseUtcOffset']) ?? '',
1088
+ standardName,
1089
+ displayName,
1090
+ daylightName: getString(value['daylightName'] ?? value['DaylightName']) ??
1091
+ standardName,
1092
+ name: getString(value['name'] ?? value['Name']) ?? displayName,
1093
+ };
1094
+ }
1095
+ normalizeCurrencyUnit(value) {
1096
+ if (!isRecord(value)) {
1097
+ return FIXED_CURRENCIES[0].unit;
1098
+ }
1099
+ return {
1100
+ en: getString(value['en']) ?? FIXED_CURRENCIES[0].unit.en,
1101
+ ar: getString(value['ar']) ?? FIXED_CURRENCIES[0].unit.ar,
1102
+ };
1103
+ }
1104
+ normalizeGeneralConfigurationSnapshot(value) {
1105
+ return {
1106
+ defaultExploreView: normalizeExploreDefaultView(value.defaultExploreView),
1107
+ showDesignSwitcher: normalizeShowDesignSwitcher(value.showDesignSwitcher),
1108
+ };
1109
+ }
1110
+ normalizeTimezoneSnapshot(value) {
1111
+ const defaults = this.getDefaultTimezoneValue();
1112
+ return {
1113
+ region: value.region ?? defaults.region,
1114
+ dateFormat: value.dateFormat ?? defaults.dateFormat,
1115
+ timeFormat: value.timeFormat ?? defaults.timeFormat,
1116
+ };
1117
+ }
1118
+ normalizeCurrencySnapshot(value) {
1119
+ const code = this.resolveCurrencyCode(value);
1120
+ const currency = FIXED_CURRENCIES.find((item) => item.code === code) ??
1121
+ FIXED_CURRENCIES[0];
1122
+ const defaultFormat = this.buildCurrencyFormatOptions(currency)[0]?.id ?? null;
1123
+ return {
1124
+ name: currency.code,
1125
+ unit: currency.unit,
1126
+ format: value.format ?? defaultFormat,
1127
+ };
1128
+ }
1129
+ normalizeWeekendSnapshot(value) {
1130
+ return {
1131
+ days: normalizeWeekendDays(value.days),
1132
+ };
1133
+ }
1134
+ normalizeTimeSheetPolicySnapshot(value, hoursPerDay) {
1135
+ return {
1136
+ frequency: normalizeFrequency(value.frequency),
1137
+ maxDailyHours: Math.max(clampNumber(hoursPerDay, 0, 24), clampNumber(value.maxDailyHours, 1, 24)),
1138
+ requireDescription: value.requireDescription,
1139
+ allowFutureEntry: value.allowFutureEntry,
1140
+ allowBackdatedEntry: value.allowBackdatedEntry,
1141
+ autoLockOnApprove: value.autoLockOnApprove,
1142
+ submissionDeadlineDays: clampNumber(value.submissionDeadlineDays, 0, 90),
1143
+ };
1144
+ }
1145
+ normalizeGlobalWorkingHoursSnapshot(value) {
1146
+ const defaults = this.getDefaultGlobalWorkingHoursValue();
1147
+ return {
1148
+ timeZoneId: value.timeZoneId || defaults.timeZoneId,
1149
+ workingDays: normalizeDotnetDays(value.workingDays),
1150
+ hoursPerDay: clampNumber(value.hoursPerDay, 0, 24),
1151
+ holidays: value.holidays,
1152
+ };
1153
+ }
1154
+ createFallbackSetting(key) {
1155
+ switch (key) {
1156
+ case 'Identity':
1157
+ return {
1158
+ key,
1159
+ label: key,
1160
+ description: key,
1161
+ value: { ...DEFAULT_GENERAL_CONFIGURATION },
1162
+ };
1163
+ case 'Holidays':
1164
+ return { key, label: key, description: key, value: { data: [] } };
1165
+ case 'Timezone':
1166
+ return {
1167
+ key,
1168
+ label: key,
1169
+ description: key,
1170
+ value: this.getDefaultTimezoneValue(),
1171
+ };
1172
+ case 'Currency':
1173
+ return {
1174
+ key,
1175
+ label: key,
1176
+ description: key,
1177
+ value: this.getDefaultCurrencyValue(),
1178
+ };
1179
+ case 'Weekend':
1180
+ return { key, label: key, description: key, value: { days: [] } };
1181
+ case 'TimeSheetPolicy':
1182
+ return {
1183
+ key,
1184
+ label: key,
1185
+ description: key,
1186
+ value: this.getDefaultTimeSheetPolicyValue(),
1187
+ };
1188
+ case 'GlobalWorkingHours':
1189
+ return {
1190
+ key,
1191
+ label: key,
1192
+ description: key,
1193
+ value: this.getDefaultGlobalWorkingHoursValue(),
1194
+ };
1195
+ }
1196
+ }
1197
+ getDefaultTimezoneValue() {
1198
+ const region = this.timezones()[0] ?? null;
1199
+ const defaultDateFormat = this.dateFormatOptions().find((option) => option.isSelected)?.id ??
1200
+ this.dateFormatOptions()[0]?.id ??
1201
+ null;
1202
+ const defaultTimeFormat = this.timeFormatOptions().find((option) => option.isSelected)?.id ??
1203
+ this.timeFormatOptions()[0]?.id ??
1204
+ null;
1205
+ return {
1206
+ region: region
1207
+ ? {
1208
+ ...region,
1209
+ name: region.name ?? region.displayName,
1210
+ }
1211
+ : null,
1212
+ dateFormat: defaultDateFormat,
1213
+ timeFormat: defaultTimeFormat,
1214
+ };
1215
+ }
1216
+ getDefaultCurrencyValue() {
1217
+ const currency = FIXED_CURRENCIES[0];
1218
+ return {
1219
+ name: currency.code,
1220
+ unit: currency.unit,
1221
+ format: this.buildCurrencyFormatOptions(currency)[0]?.id ?? null,
1222
+ };
1223
+ }
1224
+ getDefaultTimeSheetPolicyValue() {
1225
+ return {
1226
+ frequency: 'Weekly',
1227
+ maxDailyHours: 24,
1228
+ requireDescription: false,
1229
+ allowFutureEntry: false,
1230
+ allowBackdatedEntry: true,
1231
+ autoLockOnApprove: true,
1232
+ submissionDeadlineDays: 3,
1233
+ };
1234
+ }
1235
+ getDefaultGlobalWorkingHoursValue() {
1236
+ return {
1237
+ timeZoneId: this.timezones()[0]?.id ?? 'UTC',
1238
+ workingDays: [0, 1, 2, 3, 4],
1239
+ hoursPerDay: 8,
1240
+ holidays: [],
1241
+ };
1242
+ }
1243
+ resolveCurrencyCode(value) {
1244
+ const candidates = [value.name, value.unit.en, value.unit.ar].filter((candidate) => !!candidate);
1245
+ for (const candidate of candidates) {
1246
+ const normalizedCandidate = candidate.trim().toLowerCase();
1247
+ const extractedCode = candidate.match(/\(([A-Za-z]{3})\)/)?.[1]?.toUpperCase() ?? null;
1248
+ if (extractedCode) {
1249
+ const directCodeMatch = FIXED_CURRENCIES.find((item) => item.code === extractedCode);
1250
+ if (directCodeMatch) {
1251
+ return directCodeMatch.code;
1252
+ }
1253
+ }
1254
+ const match = FIXED_CURRENCIES.find((item) => item.code.toLowerCase() === normalizedCandidate ||
1255
+ item.englishLabel.toLowerCase() === normalizedCandidate ||
1256
+ item.arabicLabel.toLowerCase() === normalizedCandidate ||
1257
+ item.unit.en.toLowerCase() === normalizedCandidate ||
1258
+ item.unit.ar.toLowerCase() === normalizedCandidate);
1259
+ if (match) {
1260
+ return match.code;
1261
+ }
1262
+ }
1263
+ return FIXED_CURRENCIES[0].code;
1264
+ }
1265
+ mapFormatOptions(options, selectedKey) {
1266
+ // Read active language so the label re-translates on language switch.
1267
+ this.activeLang();
1268
+ return options.map((option) => ({
1269
+ id: option.key,
1270
+ name: this.formatOptionLabel(option.key),
1271
+ isSelected: selectedKey ? option.key === selectedKey : option.isSelected,
1272
+ }));
1273
+ }
1274
+ /**
1275
+ * Builds a localized, human-readable label for a date/time format key by
1276
+ * translating each format token (e.g. `dd-MM-yyyy` → `يوم-شهر-سنة`) while
1277
+ * preserving the original separators. Unknown characters are left as-is.
1278
+ */
1279
+ formatOptionLabel(formatKey) {
1280
+ const tokenKeyByLetter = {
1281
+ y: 'format-token-year',
1282
+ M: 'format-token-month',
1283
+ d: 'format-token-day',
1284
+ H: 'format-token-hour',
1285
+ h: 'format-token-hour',
1286
+ m: 'format-token-minute',
1287
+ s: 'format-token-second',
1288
+ t: 'format-token-period',
1289
+ };
1290
+ return formatKey.replace(/y+|M+|d+|H+|h+|m+|s+|t+/g, (token) => {
1291
+ const translationKey = tokenKeyByLetter[token[0]];
1292
+ return translationKey
1293
+ ? this.transloco.translate(`system-settings.${translationKey}`)
1294
+ : token;
1295
+ });
1296
+ }
1297
+ serializeHolidaysValue(value) {
1298
+ return {
1299
+ data: value.data.map((item) => ({
1300
+ label: item.label,
1301
+ from: formatDateOnly(item.from),
1302
+ to: formatDateOnly(item.to),
1303
+ })),
1304
+ };
1305
+ }
1306
+ buildCurrencyFormatOptions(currency) {
1307
+ const unitLabel = this.getCurrencyFormatLabel(currency);
1308
+ return [
1309
+ { id: '{0:0}', name: `1000 ${unitLabel}` },
1310
+ { id: '{0:0.0}', name: `1000.0 ${unitLabel}` },
1311
+ { id: '{0:0.00}', name: `1000.00 ${unitLabel}` },
1312
+ { id: '{0:#,0}', name: `1,000 ${unitLabel}` },
1313
+ { id: '{0:#,0.0}', name: `1,000.0 ${unitLabel}` },
1314
+ { id: '{0:#,0.00}', name: `1,000.00 ${unitLabel}` },
1315
+ { id: '{0:#,0, K}', name: `1 K ${unitLabel}` },
1316
+ { id: '{0:N3}', name: `1,000 N3 ${unitLabel}` },
1317
+ ];
1318
+ }
1319
+ getCurrencyFormatLabel(currency) {
1320
+ if (!currency) {
1321
+ return '';
1322
+ }
1323
+ return this.activeLang() === 'ar' ? currency.unit.ar : currency.unit.en;
1324
+ }
1325
+ buildEndpoint(path) {
1326
+ return `${this.apiBasePath().replace(/\/$/, '')}/${path}`;
1327
+ }
1328
+ applyUpdatedSettings(settings, values) {
1329
+ const valueMap = new Map(values.map((item) => [item.key, item.value]));
1330
+ const nextSettings = settings.map((setting) => {
1331
+ if (!valueMap.has(setting.key)) {
1332
+ return setting;
1333
+ }
1334
+ return {
1335
+ ...setting,
1336
+ value: valueMap.get(setting.key),
1337
+ };
1338
+ });
1339
+ const existingKeys = new Set(nextSettings.map((setting) => setting.key));
1340
+ for (const item of values) {
1341
+ if (existingKeys.has(item.key)) {
1342
+ continue;
1343
+ }
1344
+ nextSettings.push({
1345
+ key: item.key,
1346
+ label: item.key,
1347
+ description: item.key,
1348
+ value: item.value,
1349
+ });
1350
+ }
1351
+ return nextSettings.sort((left, right) => left.key.localeCompare(right.key));
1352
+ }
1353
+ areEqual(left, right) {
1354
+ return JSON.stringify(left) === JSON.stringify(right);
1355
+ }
1356
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettings, deps: [], target: i0.ɵɵFactoryTarget.Component });
1357
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: SystemSettings, isStandalone: true, selector: "mt-system-settings", inputs: { apiBasePath: { classPropertyName: "apiBasePath", publicName: "apiBasePath", isSignal: true, isRequired: false, transformFunction: null }, backRoute: { classPropertyName: "backRoute", publicName: "backRoute", isSignal: true, isRequired: false, transformFunction: null }, formatSchema: { classPropertyName: "formatSchema", publicName: "formatSchema", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\r\n <mt-page\r\n [title]=\"t('page-title')\"\r\n [avatarIcon]=\"'general.settings-02'\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-emerald-100)',\r\n '--p-avatar-color': 'var(--p-emerald-700)',\r\n }\"\r\n (backButtonClick)=\"goBack()\"\r\n backButton\r\n >\r\n <div class=\"flex flex-col gap-3\">\r\n <div\r\n class=\"flex flex-col gap-4 md:flex-row md:items-end md:justify-between\"\r\n >\r\n <div>\r\n <h3 class=\"text-lg font-bold text-neutral-900\">\r\n {{ t(\"page-title\") }}\r\n </h3>\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems\"\r\n [styleClass]=\"'flex justify-start mx-1'\"\r\n />\r\n </div>\r\n\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [tooltip]=\"t('save-tooltip')\"\r\n [badge]=\"hasChanges() ? '!' : ''\"\r\n badgeSeverity=\"warn\"\r\n [loading]=\"updating()\"\r\n [disabled]=\"!canSaveSettings()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n\r\n @if (isLoading()) {\r\n <div class=\"space-y-4\">\r\n <p-skeleton width=\"100%\" height=\"12rem\" borderRadius=\"0.75rem\" />\r\n <p-skeleton width=\"100%\" height=\"18rem\" borderRadius=\"0.75rem\" />\r\n <p-skeleton width=\"100%\" height=\"14rem\" borderRadius=\"0.75rem\" />\r\n </div>\r\n } @else {\r\n <!--\r\n BE/HTTP errors (timezonesError, settingsError, updateError) are already\r\n surfaced as toasts by the HTTP interceptor, so they are intentionally\r\n not rendered as top-of-page banners here to avoid duplicate messaging.\r\n -->\r\n <div class=\"space-y-6\">\r\n <mt-system-settings-general-section\r\n [generalConfigurationForm]=\"form.controls.generalConfiguration\"\r\n [timezoneForm]=\"form.controls.timezone\"\r\n [currencyForm]=\"form.controls.currency\"\r\n [weekendForm]=\"form.controls.weekend\"\r\n [exploreDefaultViewOptions]=\"exploreDefaultViewOptions()\"\r\n [dateFormatOptions]=\"dateFormatOptions()\"\r\n [timeFormatOptions]=\"timeFormatOptions()\"\r\n [currencyOptions]=\"currencyOptions()\"\r\n [currencyFormatOptions]=\"currencyFormatOptions()\"\r\n [weekendOptions]=\"weekendOptions()\"\r\n />\r\n\r\n <mt-system-settings-timesheet-section\r\n [policyForm]=\"form.controls.timeSheetPolicy\"\r\n [globalWorkingHoursForm]=\"form.controls.globalWorkingHours\"\r\n [dayOptions]=\"dotnetDayOptions()\"\r\n [frequencyOptions]=\"frequencyOptions()\"\r\n [timezoneOptions]=\"timezoneOptions()\"\r\n [maxDailyBelowDaily]=\"form.errors?.['maxDailyBelowDaily'] ?? false\"\r\n />\r\n\r\n <mt-system-settings-holidays-section\r\n [holidays]=\"holidayTableRows()\"\r\n (addHoliday)=\"openAddHoliday()\"\r\n (editHoliday)=\"editHoliday($event)\"\r\n (deleteHoliday)=\"deleteHoliday($event)\"\r\n />\r\n </div>\r\n }\r\n </div>\r\n </mt-page>\r\n</ng-container>\r\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: SystemSettingsHolidaysSection, selector: "mt-system-settings-holidays-section", inputs: ["holidays"], outputs: ["addHoliday", "editHoliday", "deleteHoliday"] }, { kind: "component", type: SystemSettingsGeneralSection, selector: "mt-system-settings-general-section", inputs: ["generalConfigurationForm", "timezoneForm", "currencyForm", "weekendForm", "exploreDefaultViewOptions", "dateFormatOptions", "timeFormatOptions", "currencyOptions", "currencyFormatOptions", "weekendOptions"] }, { kind: "component", type: SystemSettingsTimeSheetSection, selector: "mt-system-settings-timesheet-section", inputs: ["policyForm", "globalWorkingHoursForm", "dayOptions", "frequencyOptions", "timezoneOptions", "maxDailyBelowDaily"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1358
+ }
1359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SystemSettings, decorators: [{
1360
+ type: Component,
1361
+ args: [{ selector: 'mt-system-settings', imports: [
1362
+ CommonModule,
1363
+ ReactiveFormsModule,
1364
+ TranslocoDirective,
1365
+ Page,
1366
+ Button,
1367
+ Breadcrumb,
1368
+ SkeletonModule,
1369
+ SystemSettingsHolidaysSection,
1370
+ SystemSettingsGeneralSection,
1371
+ SystemSettingsTimeSheetSection,
1372
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'system-settings'\">\r\n <mt-page\r\n [title]=\"t('page-title')\"\r\n [avatarIcon]=\"'general.settings-02'\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-emerald-100)',\r\n '--p-avatar-color': 'var(--p-emerald-700)',\r\n }\"\r\n (backButtonClick)=\"goBack()\"\r\n backButton\r\n >\r\n <div class=\"flex flex-col gap-3\">\r\n <div\r\n class=\"flex flex-col gap-4 md:flex-row md:items-end md:justify-between\"\r\n >\r\n <div>\r\n <h3 class=\"text-lg font-bold text-neutral-900\">\r\n {{ t(\"page-title\") }}\r\n </h3>\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems\"\r\n [styleClass]=\"'flex justify-start mx-1'\"\r\n />\r\n </div>\r\n\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [tooltip]=\"t('save-tooltip')\"\r\n [badge]=\"hasChanges() ? '!' : ''\"\r\n badgeSeverity=\"warn\"\r\n [loading]=\"updating()\"\r\n [disabled]=\"!canSaveSettings()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n\r\n @if (isLoading()) {\r\n <div class=\"space-y-4\">\r\n <p-skeleton width=\"100%\" height=\"12rem\" borderRadius=\"0.75rem\" />\r\n <p-skeleton width=\"100%\" height=\"18rem\" borderRadius=\"0.75rem\" />\r\n <p-skeleton width=\"100%\" height=\"14rem\" borderRadius=\"0.75rem\" />\r\n </div>\r\n } @else {\r\n <!--\r\n BE/HTTP errors (timezonesError, settingsError, updateError) are already\r\n surfaced as toasts by the HTTP interceptor, so they are intentionally\r\n not rendered as top-of-page banners here to avoid duplicate messaging.\r\n -->\r\n <div class=\"space-y-6\">\r\n <mt-system-settings-general-section\r\n [generalConfigurationForm]=\"form.controls.generalConfiguration\"\r\n [timezoneForm]=\"form.controls.timezone\"\r\n [currencyForm]=\"form.controls.currency\"\r\n [weekendForm]=\"form.controls.weekend\"\r\n [exploreDefaultViewOptions]=\"exploreDefaultViewOptions()\"\r\n [dateFormatOptions]=\"dateFormatOptions()\"\r\n [timeFormatOptions]=\"timeFormatOptions()\"\r\n [currencyOptions]=\"currencyOptions()\"\r\n [currencyFormatOptions]=\"currencyFormatOptions()\"\r\n [weekendOptions]=\"weekendOptions()\"\r\n />\r\n\r\n <mt-system-settings-timesheet-section\r\n [policyForm]=\"form.controls.timeSheetPolicy\"\r\n [globalWorkingHoursForm]=\"form.controls.globalWorkingHours\"\r\n [dayOptions]=\"dotnetDayOptions()\"\r\n [frequencyOptions]=\"frequencyOptions()\"\r\n [timezoneOptions]=\"timezoneOptions()\"\r\n [maxDailyBelowDaily]=\"form.errors?.['maxDailyBelowDaily'] ?? false\"\r\n />\r\n\r\n <mt-system-settings-holidays-section\r\n [holidays]=\"holidayTableRows()\"\r\n (addHoliday)=\"openAddHoliday()\"\r\n (editHoliday)=\"editHoliday($event)\"\r\n (deleteHoliday)=\"deleteHoliday($event)\"\r\n />\r\n </div>\r\n }\r\n </div>\r\n </mt-page>\r\n</ng-container>\r\n", styles: [":host{display:block}\n"] }]
1373
+ }], ctorParameters: () => [], propDecorators: { apiBasePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "apiBasePath", required: false }] }], backRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "backRoute", required: false }] }], formatSchema: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatSchema", required: false }] }] } });
1374
+ function isRecord(value) {
1375
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1376
+ }
1377
+ function getString(value) {
1378
+ if (typeof value !== 'string') {
1379
+ return null;
1380
+ }
1381
+ const normalizedValue = value.trim();
1382
+ return normalizedValue ? normalizedValue : null;
1383
+ }
1384
+ function getNullableString(value) {
1385
+ if (typeof value !== 'string') {
1386
+ return null;
1387
+ }
1388
+ return value.trim() || null;
1389
+ }
1390
+ function getNullableNumber(value) {
1391
+ if (typeof value === 'number' && !Number.isNaN(value)) {
1392
+ return value;
1393
+ }
1394
+ if (typeof value === 'string' && value.trim()) {
1395
+ const parsedValue = Number(value);
1396
+ return Number.isNaN(parsedValue) ? null : parsedValue;
1397
+ }
1398
+ return null;
1399
+ }
1400
+ function getNumber(value) {
1401
+ return getNullableNumber(value);
1402
+ }
1403
+ function getBoolean(value) {
1404
+ if (typeof value === 'boolean') {
1405
+ return value;
1406
+ }
1407
+ if (typeof value === 'string') {
1408
+ const normalizedValue = value.trim().toLowerCase();
1409
+ if (normalizedValue === 'true') {
1410
+ return true;
1411
+ }
1412
+ if (normalizedValue === 'false') {
1413
+ return false;
1414
+ }
1415
+ }
1416
+ return null;
1417
+ }
1418
+ function getStringArray(value) {
1419
+ if (!Array.isArray(value)) {
1420
+ return null;
1421
+ }
1422
+ return value
1423
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
1424
+ .filter(Boolean);
1425
+ }
1426
+ function getNumberArray(value) {
1427
+ if (!Array.isArray(value)) {
1428
+ return null;
1429
+ }
1430
+ return value
1431
+ .map((item) => (typeof item === 'number' ? item : Number(item)))
1432
+ .filter((item) => Number.isInteger(item));
1433
+ }
1434
+ /**
1435
+ * The design switcher is opt-in, so anything that is not an explicit `true`
1436
+ * (or the string a settings record may round-trip it as) reads as hidden.
1437
+ */
1438
+ function normalizeShowDesignSwitcher(value) {
1439
+ return value === true || value === 'true' || value === 'True';
1440
+ }
1441
+ function normalizeExploreDefaultView(value) {
1442
+ return EXPLORE_DEFAULT_VIEW_OPTIONS.some((option) => option.id === value)
1443
+ ? value
1444
+ : DEFAULT_GENERAL_CONFIGURATION.defaultExploreView;
1445
+ }
1446
+ function normalizeWeekendDays(value) {
1447
+ if (!Array.isArray(value)) {
1448
+ return [];
1449
+ }
1450
+ return value
1451
+ .map((item) => (typeof item === 'number' ? item : Number(item)))
1452
+ .filter((item) => Number.isInteger(item) && item >= 1 && item <= 7)
1453
+ .sort((left, right) => left - right);
1454
+ }
1455
+ function normalizeDotnetDays(value) {
1456
+ if (!Array.isArray(value)) {
1457
+ return [];
1458
+ }
1459
+ return Array.from(new Set(value
1460
+ .map((item) => (typeof item === 'number' ? item : Number(item)))
1461
+ .filter((item) => Number.isInteger(item) && item >= 0 && item <= 6))).sort((left, right) => left - right);
1462
+ }
1463
+ function clampNumber(value, min, max) {
1464
+ if (!Number.isFinite(value)) {
1465
+ return min;
1466
+ }
1467
+ return Math.min(Math.max(value, min), max);
1468
+ }
1469
+ const TIMESHEET_FREQUENCIES = [
1470
+ 'Daily',
1471
+ 'Weekly',
1472
+ 'BiWeekly',
1473
+ 'Monthly',
1474
+ ];
1475
+ function normalizeFrequency(value) {
1476
+ return typeof value === 'string' &&
1477
+ TIMESHEET_FREQUENCIES.includes(value)
1478
+ ? value
1479
+ : 'Weekly';
1480
+ }
1481
+ function maxDailyHoursGteHoursPerDay(group) {
1482
+ const maxDailyHours = group.get('timeSheetPolicy.maxDailyHours')?.value;
1483
+ const hoursPerDay = group.get('globalWorkingHours.hoursPerDay')?.value;
1484
+ if (typeof maxDailyHours === 'number' &&
1485
+ typeof hoursPerDay === 'number' &&
1486
+ maxDailyHours < hoursPerDay) {
1487
+ return { maxDailyBelowDaily: true };
1488
+ }
1489
+ return null;
1490
+ }
1491
+ function parseCsv(value) {
1492
+ return value
1493
+ .split(',')
1494
+ .map((item) => item.trim())
1495
+ .filter(Boolean);
1496
+ }
1497
+ function normalizeDateInput(value) {
1498
+ if (value instanceof Date) {
1499
+ if (Number.isNaN(value.getTime())) {
1500
+ return null;
1501
+ }
1502
+ return createLocalDate(value.getFullYear(), value.getMonth() + 1, value.getDate());
1503
+ }
1504
+ if (typeof value !== 'string') {
1505
+ return null;
1506
+ }
1507
+ const normalizedValue = value.trim();
1508
+ if (!normalizedValue) {
1509
+ return null;
1510
+ }
1511
+ const dateOnlyMatch = normalizedValue.match(/^(\d{4})[-/](\d{2})[-/](\d{2})/);
1512
+ if (dateOnlyMatch) {
1513
+ const [, year, month, day] = dateOnlyMatch;
1514
+ return createLocalDate(Number(year), Number(month), Number(day));
1515
+ }
1516
+ const parsedDate = new Date(normalizedValue);
1517
+ if (Number.isNaN(parsedDate.getTime())) {
1518
+ return null;
1519
+ }
1520
+ return createLocalDate(parsedDate.getFullYear(), parsedDate.getMonth() + 1, parsedDate.getDate());
1521
+ }
1522
+ function createLocalDate(year, month, day) {
1523
+ return new Date(year, month - 1, day, 12, 0, 0, 0);
1524
+ }
1525
+ function formatDateOnly(value) {
1526
+ const year = value.getFullYear();
1527
+ const month = String(value.getMonth() + 1).padStart(2, '0');
1528
+ const day = String(value.getDate()).padStart(2, '0');
1529
+ return `${year}-${month}-${day}`;
1530
+ }
1531
+
1532
+ const EMAIL_DELIVERY_PERMISSIONS = {
1533
+ controlPanel: 'CONTROL_PANEL',
1534
+ };
1535
+ const EMAIL_DELIVERY_ATTEMPT_STATUSES = [
1536
+ 'Reserved',
1537
+ 'ProviderInvocationStarted',
1538
+ 'ProviderInvocationDefinitivelyNotAccepted',
1539
+ 'ProviderResultReconciliation',
1540
+ 'ProviderResultPending',
1541
+ 'Blocked',
1542
+ 'RetryScheduled',
1543
+ 'Accepted',
1544
+ 'OutcomeUnknown',
1545
+ 'FailedPermanent',
1546
+ 'DeadLettered',
1547
+ ];
1548
+ /**
1549
+ * Activity rows the backend writes for an operator-run controlled test. They
1550
+ * carry `templateId: 0`, the caller's original `testRequestId`, and a
1551
+ * non-reversible recipient fingerprint instead of any real address. They are
1552
+ * presented as "Connection test" and are never replayable — resending a test is
1553
+ * an explicit operator action from the connection, not a replay.
1554
+ */
1555
+ const EMAIL_USER_INITIATED_TEST_OPERATION_KIND = 'UserInitiatedTest';
1556
+ function isConnectionTestAttempt(attempt) {
1557
+ return attempt.operationKind === EMAIL_USER_INITIATED_TEST_OPERATION_KIND;
1558
+ }
1559
+
1560
+ const DEFAULT_EMAIL_DELIVERY_OPTIONS = {
1561
+ apiBasePath: 'control-panel/email-delivery',
1562
+ notificationApiBasePath: 'Notifications',
1563
+ currentUserAccessibilitiesApiPath: 'identity/users/me/accessibilities',
1564
+ permissionKeys: [EMAIL_DELIVERY_PERMISSIONS.controlPanel],
1565
+ };
1566
+ const EMAIL_DELIVERY_OPTIONS = new InjectionToken('EMAIL_DELIVERY_OPTIONS', {
1567
+ providedIn: 'root',
1568
+ factory: () => DEFAULT_EMAIL_DELIVERY_OPTIONS,
1569
+ });
1570
+ function provideEmailDelivery(options = {}) {
1571
+ return makeEnvironmentProviders([
1572
+ {
1573
+ provide: EMAIL_DELIVERY_OPTIONS,
1574
+ useValue: { ...DEFAULT_EMAIL_DELIVERY_OPTIONS, ...options },
1575
+ },
1576
+ ]);
1577
+ }
1578
+
1579
+ /**
1580
+ * Resolves the signed-in user's real application accessibilities.
1581
+ *
1582
+ * `permissionKeys` describes which accessibility the feature requires; it is
1583
+ * never a list of permissions granted to the user. The grant comes only from
1584
+ * the authenticated `me/accessibilities` endpoint used by the host backend.
1585
+ */
1586
+ class EmailDeliveryAccessService {
1587
+ http = inject(HttpClient);
1588
+ options = inject(EMAIL_DELIVERY_OPTIONS);
1589
+ request$ = null;
1590
+ keys = signal([], ...(ngDevMode ? [{ debugName: "keys" }] : /* istanbul ignore next */ []));
1591
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1592
+ loaded = signal(false, ...(ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []));
1593
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1594
+ hasPermission(permission) {
1595
+ return (this.options.permissionKeys.includes(permission) &&
1596
+ this.keys().includes(permission));
1597
+ }
1598
+ load(force = false) {
1599
+ if (this.request$) {
1600
+ return this.request$;
1601
+ }
1602
+ if (!force && this.loaded()) {
1603
+ return of(this.keys());
1604
+ }
1605
+ this.loading.set(true);
1606
+ this.error.set(null);
1607
+ const request = this.http
1608
+ .get(this.options.currentUserAccessibilitiesApiPath, { headers: { noMessage: 'true' } })
1609
+ .pipe(map((response) => Array.from(new Set((response.data ?? [])
1610
+ .map((item) => item.moduleKey?.trim())
1611
+ .filter((key) => !!key)))), tap((keys) => {
1612
+ this.keys.set(keys);
1613
+ this.loaded.set(true);
1614
+ }),
1615
+ // A failed read is not an authorization decision, so `loaded` stays
1616
+ // false: caching it would turn one 500 into a permanent, silent "no
1617
+ // access" for the rest of the session, indistinguishable from a real
1618
+ // denial. The next `load()` retries instead.
1619
+ catchError((error) => {
1620
+ this.keys.set([]);
1621
+ this.error.set(error);
1622
+ return throwError(() => error);
1623
+ }), finalize(() => {
1624
+ this.loading.set(false);
1625
+ if (this.request$ === request) {
1626
+ this.request$ = null;
1627
+ }
1628
+ }), shareReplay({ bufferSize: 1, refCount: false }));
1629
+ this.request$ = request;
1630
+ return request;
1631
+ }
1632
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAccessService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1633
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAccessService, providedIn: 'root' });
1634
+ }
1635
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAccessService, decorators: [{
1636
+ type: Injectable,
1637
+ args: [{ providedIn: 'root' }]
1638
+ }] });
1639
+
1640
+ function createEmailTestRequestId() {
1641
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
1642
+ return crypto.randomUUID();
1643
+ }
1644
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => {
1645
+ const random = Math.floor(Math.random() * 16);
1646
+ const value = character === 'x' ? random : (random & 0x3) | 0x8;
1647
+ return value.toString(16);
1648
+ });
1649
+ }
1650
+ function parseEmailTestRecipients(value) {
1651
+ const recipients = new Map();
1652
+ for (const recipient of (value ?? '')
1653
+ .split(/[\s,;]+/)
1654
+ .map((item) => item.trim())
1655
+ .filter(Boolean)) {
1656
+ const key = recipient.toLowerCase();
1657
+ if (!recipients.has(key))
1658
+ recipients.set(key, recipient);
1659
+ }
1660
+ return Array.from(recipients.values());
1661
+ }
1662
+ function areEmailTestRecipientsValid(value, maximumRecipients = 10, maximumAddressLength = 256) {
1663
+ const recipients = parseEmailTestRecipients(String(value ?? ''));
1664
+ return (recipients.length >= 1 &&
1665
+ recipients.length <= maximumRecipients &&
1666
+ recipients.every((recipient) => recipient.length <= maximumAddressLength &&
1667
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)));
1668
+ }
1669
+ function collectEmailTemplatePlaceholders(...templates) {
1670
+ const keys = new Set();
1671
+ for (const template of templates) {
1672
+ for (const match of template?.matchAll(/\{\{\s*([^{}]+?)\s*\}\}/g) ?? []) {
1673
+ const key = match[1]?.trim();
1674
+ if (key)
1675
+ keys.add(key);
1676
+ }
1677
+ }
1678
+ return Array.from(keys).sort((left, right) => left.localeCompare(right));
1679
+ }
1680
+ /**
1681
+ * Maps saved template keys to the localized labels returned by the property
1682
+ * catalog while preserving the original keys used in test metadata.
1683
+ */
1684
+ function buildEmailTemplatePlaceholderFields(keys, properties, language = 'en') {
1685
+ const catalog = new Map(properties
1686
+ .map((property) => [property.key.trim(), property])
1687
+ .filter(([key]) => !!key));
1688
+ const isArabic = language.trim().toLowerCase().startsWith('ar');
1689
+ return Array.from(new Set(keys.map((key) => key.trim()).filter(Boolean)))
1690
+ .map((key) => {
1691
+ const exactProperty = catalog.get(key);
1692
+ const languageVariant = key.match(/_(en|ar)$/i)?.[1]?.toLowerCase();
1693
+ const property = exactProperty ??
1694
+ (languageVariant ? catalog.get(key.slice(0, -3)) : undefined);
1695
+ const candidates = isArabic
1696
+ ? [property?.nameAr, property?.name, property?.nameEn]
1697
+ : [property?.nameEn, property?.name, property?.nameAr];
1698
+ const baseLabel = candidates.find((value) => value?.trim())?.trim() ?? key;
1699
+ const variantLabel = languageVariant
1700
+ ? isArabic
1701
+ ? languageVariant === 'ar'
1702
+ ? 'العربية'
1703
+ : 'الإنجليزية'
1704
+ : languageVariant === 'ar'
1705
+ ? 'Arabic'
1706
+ : 'English'
1707
+ : null;
1708
+ const label = !exactProperty && property && variantLabel
1709
+ ? `${baseLabel} — ${variantLabel}`
1710
+ : baseLabel;
1711
+ return { key, label };
1712
+ })
1713
+ .sort((left, right) => left.label.localeCompare(right.label, language) ||
1714
+ left.key.localeCompare(right.key));
1715
+ }
1716
+ /**
1717
+ * Terminal request states. `Abandoned` is what retention stamps on a test that
1718
+ * was still in progress when it aged out, so it ends the poll like any other
1719
+ * settled state — without it the dialog polls its whole budget and then reports
1720
+ * a timeout for a request the backend has already closed.
1721
+ */
1722
+ const TERMINAL_EMAIL_TEST_REQUEST_STATES = [
1723
+ 'Completed',
1724
+ 'Failed',
1725
+ 'Cancelled',
1726
+ 'Canceled',
1727
+ 'Abandoned',
1728
+ ];
1729
+ function isEmailTestRequestPending(state) {
1730
+ return !TERMINAL_EMAIL_TEST_REQUEST_STATES.includes(state);
1731
+ }
1732
+
1733
+ const STATUS_DEFINITIONS = {
1734
+ // Page-level readiness, shown as a chip beside the page title instead of the
1735
+ // banner that used to sit above every tab.
1736
+ readiness: {
1737
+ ready: status('#059669', 'readiness.ready'),
1738
+ 'needs-setup': status('#D97706', 'readiness.needs-setup'),
1739
+ unavailable: status('#64748B', 'readiness.unavailable'),
1740
+ },
1741
+ connection: {
1742
+ Draft: status('#64748B', 'connection.draft'),
1743
+ PendingAuthorization: status('#D97706', 'connection.pending-authorization'),
1744
+ PendingValidation: status('#0284C7', 'connection.pending-validation'),
1745
+ Ready: status('#059669', 'connection.ready'),
1746
+ ReauthorizationRequired: status('#EA580C', 'connection.reauthorization-required'),
1747
+ ConditionalAccessRequired: status('#EA580C', 'connection.conditional-access-required'),
1748
+ Disabled: status('#64748B', 'connection.disabled'),
1749
+ Invalid: status('#DC2626', 'connection.invalid'),
1750
+ },
1751
+ attempt: {
1752
+ Reserved: status('#64748B', 'attempt.reserved'),
1753
+ ProviderInvocationStarted: status('#0284C7', 'attempt.provider-invocation-started'),
1754
+ ProviderInvocationDefinitivelyNotAccepted: status('#DC2626', 'attempt.provider-not-accepted'),
1755
+ ProviderResultReconciliation: status('#7C3AED', 'attempt.reconciling'),
1756
+ ProviderResultPending: status('#0284C7', 'attempt.pending'),
1757
+ Blocked: status('#DC2626', 'attempt.blocked'),
1758
+ RetryScheduled: status('#D97706', 'attempt.retry-scheduled'),
1759
+ Accepted: status('#059669', 'attempt.accepted'),
1760
+ OutcomeUnknown: status('#EA580C', 'attempt.outcome-unknown'),
1761
+ FailedPermanent: status('#DC2626', 'attempt.failed-permanent'),
1762
+ DeadLettered: status('#BE123C', 'attempt.dead-lettered'),
1763
+ },
1764
+ support: {
1765
+ active: status('#D97706', 'support.active'),
1766
+ 'repair-pending': status('#0284C7', 'support.repair-pending'),
1767
+ repaired: status('#059669', 'support.repaired'),
1768
+ 'acknowledgement-pending': status('#0284C7', 'support.acknowledgement-pending'),
1769
+ acknowledged: status('#64748B', 'support.acknowledged'),
1770
+ Owned: status('#059669', 'support.owned'),
1771
+ owned: status('#059669', 'support.owned'),
1772
+ Missing: status('#D97706', 'support.missing'),
1773
+ missing: status('#D97706', 'support.missing'),
1774
+ Conflict: status('#DC2626', 'support.conflict'),
1775
+ conflict: status('#DC2626', 'support.conflict'),
1776
+ IdentityIncomplete: status('#EA580C', 'support.identity-incomplete'),
1777
+ identityincomplete: status('#EA580C', 'support.identity-incomplete'),
1778
+ 'retryable-after-upgrade': status('#0284C7', 'support.retryable-after-upgrade'),
1779
+ 'repair-required': status('#EA580C', 'support.repair-required'),
1780
+ 'acknowledge-only': status('#64748B', 'support.acknowledge-only'),
1781
+ configured: status('#059669', 'support.configured'),
1782
+ 'not-configured': status('#64748B', 'support.not-configured'),
1783
+ },
1784
+ };
1785
+ class EmailDeliveryStatus {
1786
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1787
+ kind = input('support', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
1788
+ transloco = inject(TranslocoService);
1789
+ activeLanguage = toSignal(this.transloco.langChanges$, {
1790
+ initialValue: this.transloco.getActiveLang(),
1791
+ });
1792
+ statusData = computed(() => {
1793
+ this.activeLanguage();
1794
+ return {
1795
+ name: '',
1796
+ viewType: 'Status',
1797
+ value: getEmailDeliveryStatusValue(this.kind(), this.value(), (key) => this.transloco.translate(key)),
1798
+ configuration: { hideName: true },
1799
+ };
1800
+ }, ...(ngDevMode ? [{ debugName: "statusData" }] : /* istanbul ignore next */ []));
1801
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, deps: [], target: i0.ɵɵFactoryTarget.Component });
1802
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: EmailDeliveryStatus, isStandalone: true, selector: "mt-email-delivery-status", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: `<mt-entity-status [data]="statusData()" />`, isInline: true, dependencies: [{ kind: "component", type: EntityStatus, selector: "mt-entity-status", inputs: ["data", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1803
+ }
1804
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, decorators: [{
1805
+ type: Component,
1806
+ args: [{
1807
+ selector: 'mt-email-delivery-status',
1808
+ imports: [EntityStatus],
1809
+ template: `<mt-entity-status [data]="statusData()" />`,
1810
+ host: { class: 'inline-flex' },
1811
+ changeDetection: ChangeDetectionStrategy.OnPush,
1812
+ }]
1813
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: false }] }] } });
1814
+ function getEmailDeliveryStatusMap(kind, translate) {
1815
+ return Object.fromEntries(Object.entries(STATUS_DEFINITIONS[kind]).map(([key, definition]) => [
1816
+ key,
1817
+ {
1818
+ color: definition.color,
1819
+ display: translate(`email-delivery.status.${definition.translationKey}`),
1820
+ },
1821
+ ]));
1822
+ }
1823
+ function getEmailDeliveryStatusValue(kind, value, translate) {
1824
+ const definition = STATUS_DEFINITIONS[kind][value];
1825
+ return {
1826
+ key: value,
1827
+ display: definition
1828
+ ? translate(`email-delivery.status.${definition.translationKey}`)
1829
+ : value,
1830
+ color: definition?.color ?? '#64748B',
1831
+ };
1832
+ }
1833
+ function status(color, translationKey) {
1834
+ return { color, translationKey };
1835
+ }
1836
+
1837
+ class EmailDeliveryApi {
1838
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1839
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi });
1840
+ }
1841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, decorators: [{
1842
+ type: Injectable
1843
+ }] });
1844
+
1845
+ class EmailDeliveryHttpApi extends EmailDeliveryApi {
1846
+ http = inject(HttpClient);
1847
+ options = inject(EMAIL_DELIVERY_OPTIONS);
1848
+ getStatus() {
1849
+ return this.http.get(this.url('status'));
1850
+ }
1851
+ getProviders() {
1852
+ return this.http.get(this.url('providers'));
1853
+ }
1854
+ getErrorCatalog() {
1855
+ return this.http.get(this.url('error-catalog'));
1856
+ }
1857
+ getConnections() {
1858
+ return this.http.get(this.url('connections'));
1859
+ }
1860
+ getConnection(id) {
1861
+ return this.http.get(this.url(`connections/${encodeURIComponent(id)}`));
1862
+ }
1863
+ createConnection(request) {
1864
+ return this.http.post(this.url('connections'), request);
1865
+ }
1866
+ updateConnection(id, request) {
1867
+ return this.http.put(this.url(`connections/${encodeURIComponent(id)}`), request);
1868
+ }
1869
+ deleteConnection(id, concurrencyToken) {
1870
+ return this.http.delete(this.url(`connections/${encodeURIComponent(id)}`), { params: { concurrencyToken } });
1871
+ }
1872
+ bindConnection(purpose, connectionId) {
1873
+ return this.http.put(this.url(`bindings/${encodeURIComponent(purpose)}`), { connectionId });
1874
+ }
1875
+ unbindConnection(purpose) {
1876
+ return this.http.delete(this.url(`bindings/${encodeURIComponent(purpose)}`));
1877
+ }
1878
+ validateConnection(id) {
1879
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/validate`), null);
1880
+ }
1881
+ sendConnectionTest(id, request) {
1882
+ return this.http
1883
+ .post(this.url(`connections/${encodeURIComponent(id)}/test`), request, { observe: 'response' })
1884
+ .pipe(mergeMap((response) => {
1885
+ const body = response.body;
1886
+ if (isEmailSendResult(body))
1887
+ return of(body);
1888
+ // 202 means the send was accepted without a result on this response.
1889
+ // The caller must read the outcome back with the same testRequestId;
1890
+ // posting again would deliver a second real email.
1891
+ if (response.status === 202) {
1892
+ return throwError(() => body ?? PENDING_CONTROLLED_TEST);
1893
+ }
1894
+ return throwError(() => body);
1895
+ }), catchError((error) => {
1896
+ const body = error instanceof HttpErrorResponse ? error.error : error;
1897
+ return isEmailSendResult(body) ? of(body) : throwError(() => error);
1898
+ }));
1899
+ }
1900
+ getConnectionTest(connectionId, testRequestId) {
1901
+ return this.http.get(this.url(`connections/${encodeURIComponent(connectionId)}/test-requests/${encodeURIComponent(testRequestId)}`));
1902
+ }
1903
+ startDelegatedAuthorization(id) {
1904
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/delegated/start`), null);
1905
+ }
1906
+ startAdminConsent(id) {
1907
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/admin-consent/start`), null);
1908
+ }
1909
+ revokeAuthorization(id) {
1910
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/revoke`), null);
1911
+ }
1912
+ getAttempts(status, cursor, take) {
1913
+ return this.http.get(this.url('attempts'), {
1914
+ params: withoutNulls({ status, cursor, take }),
1915
+ });
1916
+ }
1917
+ replayAttempt(id, request) {
1918
+ return this.http.post(this.url(`attempts/${encodeURIComponent(id)}/replay`), request);
1919
+ }
1920
+ getTestRequest(testRequestId) {
1921
+ return this.http.get(this.url(`test-requests/${encodeURIComponent(testRequestId)}`));
1922
+ }
1923
+ sendTemplateTest(eventId, templateId, request) {
1924
+ return this.sendUserTest(this.notificationUrl(`events/${eventId}/templates/${templateId}/test-email`), request);
1925
+ }
1926
+ sendAlertTest(alertId, request) {
1927
+ return this.sendUserTest(this.notificationUrl(`alerts/${alertId}/test`), request);
1928
+ }
1929
+ sendUserTest(url, request) {
1930
+ return this.http
1931
+ .post(url, request, {
1932
+ observe: 'response',
1933
+ })
1934
+ .pipe(mergeMap((response) => {
1935
+ if (response.body?.data)
1936
+ return of(response.body.data);
1937
+ if (response.status === 202) {
1938
+ return throwError(() => PENDING_CONTROLLED_TEST);
1939
+ }
1940
+ return throwError(() => response.body);
1941
+ }));
1942
+ }
1943
+ getFinalizationQuarantine(cursor, take) {
1944
+ return this.http.get(this.url('finalization-quarantine'), { params: withoutNulls({ cursor, take }) });
1945
+ }
1946
+ retryFinalizationQuarantine(itemId, reason) {
1947
+ return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/retry`), { reason });
1948
+ }
1949
+ acknowledgeFinalizationQuarantine(itemId, reason) {
1950
+ return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/acknowledge`), { reason });
1951
+ }
1952
+ getHostEvidence(cursor, take) {
1953
+ return this.http.get(this.url('orphan-evidence'), {
1954
+ params: withoutNulls({ cursor, take }),
1955
+ });
1956
+ }
1957
+ exportHostEvidence(evidenceOccurrenceId) {
1958
+ return this.http
1959
+ .get(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/export`), {
1960
+ observe: 'response',
1961
+ responseType: 'blob',
1962
+ })
1963
+ .pipe(map((response) => toEvidenceDownload(response)));
1964
+ }
1965
+ repairHostEvidence(evidenceOccurrenceId, request) {
1966
+ return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/repair`), request);
1967
+ }
1968
+ acknowledgeHostEvidence(evidenceOccurrenceId, request) {
1969
+ return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/acknowledge`), request);
1970
+ }
1971
+ rebuildEvidenceIndex(take) {
1972
+ return this.http.post(this.url('orphan-evidence/rebuild-index'), null, { params: { take } });
1973
+ }
1974
+ url(path) {
1975
+ return joinPath(this.options.apiBasePath, path);
1976
+ }
1977
+ notificationUrl(path) {
1978
+ return joinPath(this.options.notificationApiBasePath, path);
1979
+ }
1980
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1981
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, providedIn: 'root' });
1982
+ }
1983
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, decorators: [{
1984
+ type: Injectable,
1985
+ args: [{ providedIn: 'root' }]
1986
+ }] });
1987
+ /**
1988
+ * Stand-in problem for a bare `202 Accepted` that carries no body of its own —
1989
+ * it routes the dialog to retrieval by GUID, the same as the catalog problem
1990
+ * the backend normally returns for an in-flight test.
1991
+ */
1992
+ const PENDING_CONTROLLED_TEST = {
1993
+ code: 'email.delivery_test_request_in_progress',
1994
+ category: 'pending',
1995
+ retryable: true,
1996
+ action: 'poll_status',
1997
+ readinessImpact: 'unchanged',
1998
+ correlationId: '',
1999
+ retryAfterSeconds: null,
2000
+ };
2001
+ function isEmailSendResult(value) {
2002
+ return (typeof value === 'object' &&
2003
+ value !== null &&
2004
+ typeof value.outcome === 'string' &&
2005
+ typeof value.code === 'string');
2006
+ }
2007
+ function withoutNulls(values) {
2008
+ return Object.fromEntries(Object.entries(values).filter((entry) => entry[1] !== null));
2009
+ }
2010
+ function joinPath(base, path) {
2011
+ return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
2012
+ }
2013
+ function toEvidenceDownload(response) {
2014
+ const contentDisposition = response.headers.get('content-disposition') ?? '';
2015
+ const fileNameMatch = /filename\*?=(?:UTF-8''|\")?([^\";]+)/i.exec(contentDisposition);
2016
+ return {
2017
+ blob: response.body ?? new Blob(),
2018
+ fileName: fileNameMatch
2019
+ ? decodeURIComponent(fileNameMatch[1].replace(/\"$/, ''))
2020
+ : 'email-delivery-evidence.json',
2021
+ contentType: response.headers.get('content-type') ?? 'application/octet-stream',
2022
+ };
2023
+ }
2024
+
2025
+ const PENDING_AUTHORIZATION_STORAGE_KEY = 'mt.email-delivery.pending-authorization-connection';
2026
+ class EmailDeliveryFacade {
2027
+ access = inject(EmailDeliveryAccessService);
2028
+ api = inject(EmailDeliveryHttpApi);
2029
+ transloco = inject(TranslocoService);
2030
+ toast = inject(ToastService);
2031
+ initialized = false;
2032
+ attemptsLoaded = false;
2033
+ status = signal(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
2034
+ providers = signal([], ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
2035
+ errorCatalog = signal([], ...(ngDevMode ? [{ debugName: "errorCatalog" }] : /* istanbul ignore next */ []));
2036
+ connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : /* istanbul ignore next */ []));
2037
+ selectedConnection = signal(null, ...(ngDevMode ? [{ debugName: "selectedConnection" }] : /* istanbul ignore next */ []));
2038
+ attempts = signal([], ...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
2039
+ attemptsNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "attemptsNextCursor" }] : /* istanbul ignore next */ []));
2040
+ attemptStatusFilter = signal(null, ...(ngDevMode ? [{ debugName: "attemptStatusFilter" }] : /* istanbul ignore next */ []));
2041
+ quarantine = signal([], ...(ngDevMode ? [{ debugName: "quarantine" }] : /* istanbul ignore next */ []));
2042
+ quarantineNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "quarantineNextCursor" }] : /* istanbul ignore next */ []));
2043
+ hostEvidence = signal([], ...(ngDevMode ? [{ debugName: "hostEvidence" }] : /* istanbul ignore next */ []));
2044
+ hostEvidenceNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "hostEvidenceNextCursor" }] : /* istanbul ignore next */ []));
2045
+ hostEvidenceScanBudgetExhausted = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceScanBudgetExhausted" }] : /* istanbul ignore next */ []));
2046
+ statusLoading = signal(false, ...(ngDevMode ? [{ debugName: "statusLoading" }] : /* istanbul ignore next */ []));
2047
+ catalogLoading = signal(false, ...(ngDevMode ? [{ debugName: "catalogLoading" }] : /* istanbul ignore next */ []));
2048
+ connectionsLoading = signal(false, ...(ngDevMode ? [{ debugName: "connectionsLoading" }] : /* istanbul ignore next */ []));
2049
+ detailsLoading = signal(false, ...(ngDevMode ? [{ debugName: "detailsLoading" }] : /* istanbul ignore next */ []));
2050
+ attemptsLoading = signal(false, ...(ngDevMode ? [{ debugName: "attemptsLoading" }] : /* istanbul ignore next */ []));
2051
+ quarantineLoading = signal(false, ...(ngDevMode ? [{ debugName: "quarantineLoading" }] : /* istanbul ignore next */ []));
2052
+ hostEvidenceLoading = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceLoading" }] : /* istanbul ignore next */ []));
2053
+ mutationLoading = signal(null, ...(ngDevMode ? [{ debugName: "mutationLoading" }] : /* istanbul ignore next */ []));
2054
+ lastConnectionTest = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTest" }] : /* istanbul ignore next */ []));
2055
+ lastConnectionTestRequestId = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTestRequestId" }] : /* istanbul ignore next */ []));
2056
+ permissionsLoading = this.access.loading;
2057
+ permissionsLoaded = this.access.loaded;
2058
+ permissionsError = this.access.error;
2059
+ activeConnection = computed(() => this.connections().find((connection) => connection.bindings.includes('notifications')), ...(ngDevMode ? [{ debugName: "activeConnection" }] : /* istanbul ignore next */ []));
2060
+ managementAvailable = computed(() => this.status()?.managementEnabled === true, ...(ngDevMode ? [{ debugName: "managementAvailable" }] : /* istanbul ignore next */ []));
2061
+ initialize() {
2062
+ if (this.initialized)
2063
+ return;
2064
+ this.initialized = true;
2065
+ this.loadStatus()
2066
+ .pipe(switchMap(() => forkJoin({
2067
+ catalog: this.loadCatalog(),
2068
+ connections: this.loadConnections(),
2069
+ })))
2070
+ .subscribe({ error: (error) => this.showError(error) });
2071
+ }
2072
+ hasPermission(permission) {
2073
+ return this.access.hasPermission(permission);
2074
+ }
2075
+ loadPermissions(force = false) {
2076
+ return this.access.load(force);
2077
+ }
2078
+ loadStatus() {
2079
+ this.statusLoading.set(true);
2080
+ return this.api.getStatus().pipe(tap((status) => this.status.set(status)), finalize(() => this.statusLoading.set(false)));
2081
+ }
2082
+ loadCatalog() {
2083
+ this.catalogLoading.set(true);
2084
+ return forkJoin({
2085
+ providers: this.api.getProviders(),
2086
+ errors: this.api.getErrorCatalog(),
2087
+ }).pipe(tap(({ providers, errors }) => {
2088
+ this.providers.set(providers);
2089
+ this.errorCatalog.set(errors);
2090
+ }), finalize(() => this.catalogLoading.set(false)));
2091
+ }
2092
+ loadConnections() {
2093
+ this.connectionsLoading.set(true);
2094
+ return this.api.getConnections().pipe(tap((connections) => this.connections.set(connections)), finalize(() => this.connectionsLoading.set(false)));
2095
+ }
2096
+ loadConnection(id) {
2097
+ this.detailsLoading.set(true);
2098
+ return this.api.getConnection(id).pipe(tap((details) => this.selectedConnection.set(details)), finalize(() => this.detailsLoading.set(false)));
2099
+ }
2100
+ createConnection(request) {
2101
+ this.mutationLoading.set('create');
2102
+ return this.api.createConnection(request).pipe(switchMap((details) => this.refreshConnection(details.connection.id)), tap(() => this.showSuccess('connection-created')), finalize(() => this.mutationLoading.set(null)));
2103
+ }
2104
+ updateConnection(id, request) {
2105
+ this.mutationLoading.set('update');
2106
+ return this.api.updateConnection(id, request).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-updated')), finalize(() => this.mutationLoading.set(null)));
2107
+ }
2108
+ deleteConnection(id, concurrencyToken) {
2109
+ this.mutationLoading.set('delete');
2110
+ return this.api.deleteConnection(id, concurrencyToken).pipe(this.reloadOnConflict(id), switchMap(() => forkJoin({
2111
+ connections: this.loadConnections(),
2112
+ status: this.loadStatus(),
2113
+ })), tap(() => {
2114
+ this.selectedConnection.set(null);
2115
+ this.showSuccess('connection-deleted');
2116
+ }), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
2117
+ }
2118
+ validateConnection(id) {
2119
+ this.mutationLoading.set('validate');
2120
+ return this.api.validateConnection(id).pipe(this.reloadOnConflict(id), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), tap((result) => result.isValid
2121
+ ? this.toast.success(this.describeCode(result.code))
2122
+ : this.toast.warn(this.describeCode(result.code))), finalize(() => this.mutationLoading.set(null)));
2123
+ }
2124
+ sendConnectionTest(id, request) {
2125
+ this.mutationLoading.set('test');
2126
+ this.lastConnectionTestRequestId.set(request.testRequestId);
2127
+ return this.api.sendConnectionTest(id, request).pipe(tap((result) => {
2128
+ this.lastConnectionTest.set(result);
2129
+ this.refreshActivity();
2130
+ }), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), finalize(() => this.mutationLoading.set(null)));
2131
+ }
2132
+ retrieveConnectionTest(connectionId, testRequestId) {
2133
+ this.mutationLoading.set('retrieve-test');
2134
+ return this.api.getConnectionTest(connectionId, testRequestId).pipe(tap((stored) => {
2135
+ this.lastConnectionTest.set(stored.result);
2136
+ if (stored.result)
2137
+ this.refreshActivity();
2138
+ }), finalize(() => this.mutationLoading.set(null)));
2139
+ }
2140
+ sendTemplateTest(eventId, templateId, request) {
2141
+ this.mutationLoading.set('template-test');
2142
+ return this.api
2143
+ .sendTemplateTest(eventId, templateId, request)
2144
+ .pipe(finalize(() => this.mutationLoading.set(null)));
2145
+ }
2146
+ sendAlertTest(alertId, request) {
2147
+ this.mutationLoading.set('alert-test');
2148
+ return this.api
2149
+ .sendAlertTest(alertId, request)
2150
+ .pipe(finalize(() => this.mutationLoading.set(null)));
2151
+ }
2152
+ retrieveUserTest(testRequestId) {
2153
+ this.mutationLoading.set('retrieve-user-test');
2154
+ return this.api
2155
+ .getTestRequest(testRequestId)
2156
+ .pipe(finalize(() => this.mutationLoading.set(null)));
2157
+ }
2158
+ bindConnection(id, purpose = 'notifications') {
2159
+ this.mutationLoading.set('bind');
2160
+ return this.api.bindConnection(purpose, id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-bound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
2161
+ }
2162
+ unbindConnection(id, purpose = 'notifications') {
2163
+ this.mutationLoading.set('unbind');
2164
+ return this.api.unbindConnection(purpose).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-unbound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
2165
+ }
2166
+ startAuthorization(id, kind) {
2167
+ this.mutationLoading.set(kind);
2168
+ const request = kind === 'delegated'
2169
+ ? this.api.startDelegatedAuthorization(id)
2170
+ : this.api.startAdminConsent(id);
2171
+ return request.pipe(tap(() => this.rememberAuthorizationConnection(id)), finalize(() => this.mutationLoading.set(null)));
2172
+ }
2173
+ revokeAuthorization(id) {
2174
+ this.mutationLoading.set('revoke');
2175
+ return this.api.revokeAuthorization(id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('authorization-revoked')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
2176
+ }
2177
+ loadAttempts(reset = true, status = this.attemptStatusFilter()) {
2178
+ this.attemptsLoading.set(true);
2179
+ const cursor = reset ? null : this.attemptsNextCursor();
2180
+ if (reset)
2181
+ this.attemptStatusFilter.set(status);
2182
+ return this.api.getAttempts(status, cursor, 50).pipe(tap((page) => {
2183
+ this.attemptsLoaded = true;
2184
+ this.attempts.set(reset ? page.items : mergeById(this.attempts(), page.items, 'id'));
2185
+ this.attemptsNextCursor.set(page.nextCursor);
2186
+ }), finalize(() => this.attemptsLoading.set(false)));
2187
+ }
2188
+ replayAttempt(attempt, request) {
2189
+ this.mutationLoading.set(`replay-${attempt.id}`);
2190
+ return this.api.replayAttempt(attempt.id, request).pipe(tap((result) => {
2191
+ this.attempts.update((items) => replaceById(items, result.attempt, 'id'));
2192
+ this.showSuccess('attempt-replay-scheduled');
2193
+ }), finalize(() => this.mutationLoading.set(null)));
2194
+ }
2195
+ loadQuarantine(reset = true) {
2196
+ this.quarantineLoading.set(true);
2197
+ const cursor = reset ? null : this.quarantineNextCursor();
2198
+ return this.api.getFinalizationQuarantine(cursor, 100).pipe(tap((page) => {
2199
+ this.quarantine.set(reset
2200
+ ? page.items
2201
+ : mergeById(this.quarantine(), page.items, 'itemId'));
2202
+ this.quarantineNextCursor.set(page.nextCursor);
2203
+ }), finalize(() => this.quarantineLoading.set(false)));
2204
+ }
2205
+ decideQuarantine(item, action, reason) {
2206
+ this.mutationLoading.set(`${action}-${item.itemId}`);
2207
+ const request = action === 'retry'
2208
+ ? this.api.retryFinalizationQuarantine(item.itemId, reason)
2209
+ : this.api.acknowledgeFinalizationQuarantine(item.itemId, reason);
2210
+ return request.pipe(switchMap((updated) => this.loadQuarantine(true).pipe(map(() => updated))), tap(() => {
2211
+ this.showSuccess(action === 'retry'
2212
+ ? 'quarantine-retry-submitted'
2213
+ : 'quarantine-acknowledged');
2214
+ }), finalize(() => this.mutationLoading.set(null)));
2215
+ }
2216
+ loadHostEvidence(reset = true) {
2217
+ this.hostEvidenceLoading.set(true);
2218
+ const cursor = reset ? null : this.hostEvidenceNextCursor();
2219
+ return this.api.getHostEvidence(cursor, 100).pipe(tap((page) => {
2220
+ this.hostEvidence.set(reset
2221
+ ? page.items
2222
+ : mergeHostEvidence(this.hostEvidence(), page.items));
2223
+ this.hostEvidenceNextCursor.set(page.nextCursor);
2224
+ this.hostEvidenceScanBudgetExhausted.set(page.scanBudgetExhausted);
2225
+ }), finalize(() => this.hostEvidenceLoading.set(false)));
2226
+ }
2227
+ exportHostEvidence(evidenceOccurrenceId) {
2228
+ this.mutationLoading.set(`export-${evidenceOccurrenceId}`);
2229
+ return this.api
2230
+ .exportHostEvidence(evidenceOccurrenceId)
2231
+ .pipe(finalize(() => this.mutationLoading.set(null)));
2232
+ }
2233
+ decideHostEvidence(item, action, reason) {
2234
+ if (!item.evidenceOccurrenceId || !item.decisionToken) {
2235
+ return throwError(() => new Error('Incomplete evidence identity'));
2236
+ }
2237
+ this.mutationLoading.set(`${action}-${item.evidenceOccurrenceId}`);
2238
+ const request = {
2239
+ reason,
2240
+ decisionToken: item.decisionToken,
2241
+ };
2242
+ const operation = action === 'repair'
2243
+ ? this.api.repairHostEvidence(item.evidenceOccurrenceId, request)
2244
+ : this.api.acknowledgeHostEvidence(item.evidenceOccurrenceId, request);
2245
+ return operation.pipe(switchMap((updated) => this.loadHostEvidence(true).pipe(map(() => updated))), tap(() => {
2246
+ this.showSuccess(action === 'repair' ? 'evidence-repaired' : 'evidence-acknowledged');
2247
+ }), finalize(() => this.mutationLoading.set(null)));
2248
+ }
2249
+ rebuildEvidenceIndex(take) {
2250
+ this.mutationLoading.set('rebuild-evidence');
2251
+ return this.api.rebuildEvidenceIndex(take).pipe(switchMap((result) => this.loadHostEvidence(true).pipe(map(() => result))), tap(() => this.showSuccess('evidence-index-rebuilt')), finalize(() => this.mutationLoading.set(null)));
2252
+ }
2253
+ providerByKey(key) {
2254
+ return this.providers().find((provider) => provider.key === key) ?? null;
2255
+ }
2256
+ describeCode(code) {
2257
+ if (!code)
2258
+ return this.transloco.translate('email-delivery.unknown-code');
2259
+ const key = `email-delivery.codes.${code}`;
2260
+ const translated = this.transloco.translate(key);
2261
+ if (translated !== key)
2262
+ return translated;
2263
+ const contract = this.errorCatalog().find((entry) => entry.code === code);
2264
+ if (!contract || contract.frontendAction === 'none')
2265
+ return code;
2266
+ const actionKey = `email-delivery.error-actions.${contract.frontendAction}`;
2267
+ const action = this.transloco.translate(actionKey);
2268
+ return action === actionKey ? code : `${action} (${code})`;
2269
+ }
2270
+ /**
2271
+ * The one sentence every failure path shows. It prefers the backend's stable
2272
+ * code, falls back to the transport status the contract assigns a required
2273
+ * behaviour to (401/403/409/422/428/429/503), and always appends the
2274
+ * correlation ID when one is available — that identifier is what support asks
2275
+ * for, and it is the only part of a failure this page is allowed to keep.
2276
+ */
2277
+ describeError(error) {
2278
+ const failure = extractFailure(error);
2279
+ const parts = [this.describeFailure(failure)];
2280
+ if (failure.retryAfterSeconds !== null) {
2281
+ parts.push(this.transloco.translate('email-delivery.retry-after', {
2282
+ seconds: failure.retryAfterSeconds,
2283
+ }));
2284
+ }
2285
+ if (failure.correlationId) {
2286
+ parts.push(`${this.transloco.translate('email-delivery.correlation-id')}: ${failure.correlationId}`);
2287
+ }
2288
+ return parts.join(' ');
2289
+ }
2290
+ errorAction(error) {
2291
+ const failure = extractFailure(error);
2292
+ return (failure.action ||
2293
+ this.errorCatalog().find((entry) => entry.code === failure.code)
2294
+ ?.frontendAction ||
2295
+ statusAction(failure.status));
2296
+ }
2297
+ showError(error) {
2298
+ this.toast.error(this.describeError(error));
2299
+ }
2300
+ /**
2301
+ * A `409` means the server state moved under this page. The contract requires
2302
+ * reloading it rather than re-submitting over it, so every mutation that can
2303
+ * conflict routes through here: the latest connection, list and readiness are
2304
+ * pulled back before the caller sees the rejection.
2305
+ */
2306
+ reloadOnConflict(id) {
2307
+ return catchError((error) => {
2308
+ if (statusOf(error) !== 409)
2309
+ return throwError(() => error);
2310
+ const reload = id
2311
+ ? this.refreshConnection(id).pipe(map(() => undefined))
2312
+ : forkJoin({
2313
+ connections: this.loadConnections(),
2314
+ status: this.loadStatus(),
2315
+ }).pipe(map(() => undefined));
2316
+ reload.subscribe({ error: () => undefined });
2317
+ return throwError(() => error);
2318
+ });
2319
+ }
2320
+ describeFailure(failure) {
2321
+ const translated = failure.code ? this.translateCode(failure.code) : null;
2322
+ if (translated)
2323
+ return translated;
2324
+ const statusMessage = this.translateStatus(failure.status);
2325
+ if (statusMessage) {
2326
+ return failure.code
2327
+ ? `${statusMessage} (${failure.code})`
2328
+ : statusMessage;
2329
+ }
2330
+ return this.describeCode(failure.code);
2331
+ }
2332
+ translateCode(code) {
2333
+ const key = `email-delivery.codes.${code}`;
2334
+ const translated = this.transloco.translate(key);
2335
+ return translated === key ? null : translated;
2336
+ }
2337
+ translateStatus(status) {
2338
+ const key = status === null ? null : HTTP_STATUS_KEYS[status];
2339
+ if (!key)
2340
+ return null;
2341
+ const fullKey = `email-delivery.http.${key}`;
2342
+ const translated = this.transloco.translate(fullKey);
2343
+ return translated === fullKey ? null : translated;
2344
+ }
2345
+ takePendingAuthorizationConnection() {
2346
+ if (typeof sessionStorage === 'undefined')
2347
+ return null;
2348
+ const id = sessionStorage.getItem(PENDING_AUTHORIZATION_STORAGE_KEY);
2349
+ sessionStorage.removeItem(PENDING_AUTHORIZATION_STORAGE_KEY);
2350
+ return id;
2351
+ }
2352
+ /**
2353
+ * A completed controlled test writes its own Activity row, so the loaded
2354
+ * Activity list is stale the moment the test returns. Only refresh once
2355
+ * Activity has been opened — before that its own load covers it, and the
2356
+ * status filter in effect there is preserved.
2357
+ */
2358
+ refreshActivity() {
2359
+ if (!this.attemptsLoaded)
2360
+ return;
2361
+ this.loadAttempts(true).subscribe({ error: () => undefined });
2362
+ }
2363
+ refreshConnection(id) {
2364
+ return forkJoin({
2365
+ details: this.api.getConnection(id),
2366
+ connections: this.api.getConnections(),
2367
+ status: this.api.getStatus(),
2368
+ }).pipe(tap(({ details, connections, status }) => {
2369
+ this.selectedConnection.set(details);
2370
+ this.connections.set(connections);
2371
+ this.status.set(status);
2372
+ }), map(({ details }) => details));
2373
+ }
2374
+ rememberAuthorizationConnection(id) {
2375
+ if (typeof sessionStorage === 'undefined')
2376
+ return;
2377
+ sessionStorage.setItem(PENDING_AUTHORIZATION_STORAGE_KEY, id);
2378
+ }
2379
+ showSuccess(key) {
2380
+ this.toast.success(this.transloco.translate(`email-delivery.${key}`));
2381
+ }
2382
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2383
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, providedIn: 'root' });
2384
+ }
2385
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, decorators: [{
2386
+ type: Injectable,
2387
+ args: [{ providedIn: 'root' }]
2388
+ }] });
2389
+ const HTTP_STATUS_KEYS = {
2390
+ 401: 'unauthorized',
2391
+ 403: 'forbidden',
2392
+ 409: 'conflict',
2393
+ 422: 'unprocessable',
2394
+ 428: 'authorization-required',
2395
+ 429: 'too-many-requests',
2396
+ 503: 'unavailable',
2397
+ };
2398
+ const HTTP_STATUS_ACTIONS = {
2399
+ 403: 'request_permission',
2400
+ 409: 'refresh',
2401
+ 422: 'correct_configuration',
2402
+ 428: 'authorize',
2403
+ 429: 'retry_later',
2404
+ 503: 'retry_later',
2405
+ };
2406
+ function statusAction(status) {
2407
+ return status === null ? null : (HTTP_STATUS_ACTIONS[status] ?? null);
2408
+ }
2409
+ function statusOf(error) {
2410
+ return error instanceof HttpErrorResponse ? error.status : null;
2411
+ }
2412
+ function extractFailure(error) {
2413
+ const body = error instanceof HttpErrorResponse ? error.error : error;
2414
+ const problem = readProblem(body);
2415
+ const headers = error instanceof HttpErrorResponse ? error.headers : null;
2416
+ return {
2417
+ code: problem?.code ?? null,
2418
+ action: problem?.action || null,
2419
+ correlationId: problem?.correlationId ||
2420
+ headers?.get('x-correlation-id') ||
2421
+ headers?.get('correlation-id') ||
2422
+ null,
2423
+ retryAfterSeconds: problem?.retryAfterSeconds ??
2424
+ parseRetryAfter(headers?.get('retry-after') ?? null),
2425
+ status: statusOf(error),
2426
+ };
2427
+ }
2428
+ /**
2429
+ * Two envelopes reach this page. The email contract answers with the problem
2430
+ * shape itself; Gateway rejects a request before it ever gets there and answers
2431
+ * with `{ errors: { code, message } }` — that is how `REQ_001` arrives, and
2432
+ * reading only the first shape lost it.
2433
+ */
2434
+ function readProblem(body) {
2435
+ if (typeof body !== 'object' || body === null)
2436
+ return null;
2437
+ const problem = body;
2438
+ if (typeof problem.code === 'string')
2439
+ return problem;
2440
+ const gatewayCode = problem.errors?.code;
2441
+ if (typeof gatewayCode === 'string') {
2442
+ return { ...problem, code: gatewayCode };
2443
+ }
2444
+ return null;
2445
+ }
2446
+ /**
2447
+ * `Retry-After` is either a delay in seconds or an HTTP date. Both become a
2448
+ * whole number of seconds so the message can say when to try again; anything
2449
+ * unparseable or already in the past is simply not offered.
2450
+ */
2451
+ function parseRetryAfter(value) {
2452
+ if (!value)
2453
+ return null;
2454
+ const seconds = Number(value);
2455
+ if (Number.isFinite(seconds)) {
2456
+ return seconds > 0 ? Math.ceil(seconds) : null;
2457
+ }
2458
+ const date = Date.parse(value);
2459
+ if (Number.isNaN(date))
2460
+ return null;
2461
+ const delta = Math.ceil((date - Date.now()) / 1000);
2462
+ return delta > 0 ? delta : null;
2463
+ }
2464
+ function mergeById(current, incoming, key) {
2465
+ const values = new Map(current.map((item) => [item[key], item]));
2466
+ incoming.forEach((item) => values.set(item[key], item));
2467
+ return Array.from(values.values());
2468
+ }
2469
+ function replaceById(current, replacement, key) {
2470
+ return current.map((item) => item[key] === replacement[key] ? replacement : item);
2471
+ }
2472
+ function mergeHostEvidence(current, incoming) {
2473
+ const identity = (item) => item.evidenceOccurrenceId ?? item.itemId;
2474
+ const values = new Map(current.map((item) => [identity(item), item]));
2475
+ incoming.forEach((item) => values.set(identity(item), item));
2476
+ return Array.from(values.values());
2477
+ }
2478
+
2479
+ class EmailUserTestDialog {
2480
+ kind = input.required(...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
2481
+ eventId = input(null, ...(ngDevMode ? [{ debugName: "eventId" }] : /* istanbul ignore next */ []));
2482
+ templateId = input(null, ...(ngDevMode ? [{ debugName: "templateId" }] : /* istanbul ignore next */ []));
2483
+ alertId = input(null, ...(ngDevMode ? [{ debugName: "alertId" }] : /* istanbul ignore next */ []));
2484
+ sourceLabel = input('', ...(ngDevMode ? [{ debugName: "sourceLabel" }] : /* istanbul ignore next */ []));
2485
+ placeholderKeys = input([], ...(ngDevMode ? [{ debugName: "placeholderKeys" }] : /* istanbul ignore next */ []));
2486
+ placeholderFields = input([], ...(ngDevMode ? [{ debugName: "placeholderFields" }] : /* istanbul ignore next */ []));
2487
+ modal = inject(ModalService);
2488
+ ref = inject(ModalRef);
2489
+ destroyRef = inject(DestroyRef);
2490
+ transloco = inject(TranslocoService);
2491
+ facade = inject(EmailDeliveryFacade);
2492
+ // `mt-field-validation` renders whichever error carries a `message`; a bare
2493
+ // `true` falls through to the generic "invalid <key>" fallback, which is how
2494
+ // a mistyped address ends up disabling Send with nothing said about why. The
2495
+ // message is read at validation time so it follows the active language.
2496
+ recipients = new FormControl('', {
2497
+ nonNullable: true,
2498
+ validators: [
2499
+ Validators.required,
2500
+ (control) => !String(control.value ?? '').trim() ||
2501
+ areEmailTestRecipientsValid(control.value)
2502
+ ? null
2503
+ : { invalidRecipients: { message: this.t('recipients-invalid') } },
2504
+ ],
2505
+ });
2506
+ metadataControls = signal([], ...(ngDevMode ? [{ debugName: "metadataControls" }] : /* istanbul ignore next */ []));
2507
+ testRequestId = signal(createEmailTestRequestId(), ...(ngDevMode ? [{ debugName: "testRequestId" }] : /* istanbul ignore next */ []));
2508
+ timedOut = signal(false, ...(ngDevMode ? [{ debugName: "timedOut" }] : /* istanbul ignore next */ []));
2509
+ polling = signal(false, ...(ngDevMode ? [{ debugName: "polling" }] : /* istanbul ignore next */ []));
2510
+ result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
2511
+ pollIndex = 0;
2512
+ constructor() {
2513
+ effect(() => {
2514
+ const current = new Map(this.metadataControls().map((item) => [item.key, item.control.value]));
2515
+ const fields = new Map();
2516
+ for (const field of this.placeholderFields()) {
2517
+ const key = field.key.trim();
2518
+ if (!key || fields.has(key))
2519
+ continue;
2520
+ fields.set(key, {
2521
+ key,
2522
+ label: field.label.trim() || key,
2523
+ });
2524
+ }
2525
+ for (const rawKey of this.placeholderKeys()) {
2526
+ const key = rawKey.trim();
2527
+ if (key && !fields.has(key))
2528
+ fields.set(key, { key, label: key });
2529
+ }
2530
+ const placeholders = Array.from(fields.values()).sort((left, right) => left.label.localeCompare(right.label) ||
2531
+ left.key.localeCompare(right.key));
2532
+ if (placeholders.length === this.metadataControls().length &&
2533
+ placeholders.every((field, index) => {
2534
+ const currentField = this.metadataControls()[index];
2535
+ return (currentField?.key === field.key &&
2536
+ currentField.label === field.label);
2537
+ })) {
2538
+ return;
2539
+ }
2540
+ this.metadataControls.set(placeholders.map(({ key, label }) => ({
2541
+ key,
2542
+ label,
2543
+ control: new FormControl(current.get(key) ?? '', {
2544
+ nonNullable: true,
2545
+ }),
2546
+ })));
2547
+ });
2548
+ }
2549
+ canSubmit() {
2550
+ return (this.recipients.valid &&
2551
+ !this.result() &&
2552
+ !this.polling() &&
2553
+ this.facade.mutationLoading() === null &&
2554
+ this.hasValidTarget());
2555
+ }
2556
+ submit() {
2557
+ this.recipients.markAsTouched();
2558
+ if (!this.canSubmit())
2559
+ return;
2560
+ const request = {
2561
+ testRequestId: this.testRequestId(),
2562
+ emails: parseEmailTestRecipients(this.recipients.value),
2563
+ metadata: Object.fromEntries(this.metadataControls().map(({ key, control }) => [key, control.value])),
2564
+ };
2565
+ const operation = this.operation(request);
2566
+ if (!operation)
2567
+ return;
2568
+ operation.subscribe({
2569
+ next: (result) => this.acceptResult(result),
2570
+ error: (error) => {
2571
+ if (isAmbiguousTimeout(error) ||
2572
+ this.facade.errorAction(error) === 'poll_status') {
2573
+ this.startPolling();
2574
+ return;
2575
+ }
2576
+ this.facade.showError(error);
2577
+ if (isTestRequestConflict(error)) {
2578
+ this.testRequestId.set(createEmailTestRequestId());
2579
+ }
2580
+ },
2581
+ });
2582
+ }
2583
+ retrieve() {
2584
+ if (this.polling())
2585
+ return;
2586
+ this.retrieveStoredResult(false);
2587
+ }
2588
+ close() {
2589
+ this.ref.close(this.result());
2590
+ }
2591
+ operation(request) {
2592
+ if (this.kind() === 'template') {
2593
+ const eventId = this.eventId();
2594
+ const templateId = this.templateId();
2595
+ return eventId != null && templateId != null
2596
+ ? this.facade.sendTemplateTest(eventId, templateId, request)
2597
+ : null;
2598
+ }
2599
+ const alertId = this.alertId();
2600
+ return alertId != null ? this.facade.sendAlertTest(alertId, request) : null;
2601
+ }
2602
+ hasValidTarget() {
2603
+ return this.kind() === 'template'
2604
+ ? this.eventId() != null && this.templateId() != null
2605
+ : this.alertId() != null;
2606
+ }
2607
+ acceptResult(result) {
2608
+ if (isEmailTestRequestPending(result.requestState)) {
2609
+ this.startPolling();
2610
+ return;
2611
+ }
2612
+ this.result.set(result);
2613
+ this.timedOut.set(false);
2614
+ this.polling.set(false);
2615
+ }
2616
+ /**
2617
+ * Retrieval in progress is not the same thing as a lost result. A `202` and a
2618
+ * pending request state are the normal accepted path, so they only raise
2619
+ * `polling`; `timedOut` is reserved for the poll budget running out or a
2620
+ * retrieval that failed, which are the states that actually ask the operator
2621
+ * to act.
2622
+ */
2623
+ startPolling() {
2624
+ if (this.polling())
2625
+ return;
2626
+ this.timedOut.set(false);
2627
+ this.polling.set(true);
2628
+ this.pollIndex = 0;
2629
+ this.schedulePoll();
2630
+ }
2631
+ schedulePoll() {
2632
+ const delay = POLL_DELAYS_MS[this.pollIndex++];
2633
+ if (delay === undefined) {
2634
+ this.polling.set(false);
2635
+ this.timedOut.set(true);
2636
+ return;
2637
+ }
2638
+ timer(delay)
2639
+ .pipe(takeUntilDestroyed(this.destroyRef))
2640
+ .subscribe(() => this.retrieveStoredResult(true));
2641
+ }
2642
+ retrieveStoredResult(fromPoll) {
2643
+ this.facade.retrieveUserTest(this.testRequestId()).subscribe({
2644
+ next: (result) => {
2645
+ if (fromPoll && isEmailTestRequestPending(result.requestState)) {
2646
+ this.schedulePoll();
2647
+ return;
2648
+ }
2649
+ this.acceptResult(result);
2650
+ },
2651
+ error: (error) => {
2652
+ if (fromPoll && this.facade.errorAction(error) === 'poll_status') {
2653
+ this.schedulePoll();
2654
+ return;
2655
+ }
2656
+ this.polling.set(false);
2657
+ this.timedOut.set(true);
2658
+ this.facade.showError(error);
2659
+ },
2660
+ });
2661
+ }
2662
+ t(key) {
2663
+ return this.transloco.translate(`email-delivery.user-test.${key}`);
2664
+ }
2665
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailUserTestDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
2666
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailUserTestDialog, isStandalone: true, selector: "mt-email-user-test-dialog", inputs: { kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: true, transformFunction: null }, eventId: { classPropertyName: "eventId", publicName: "eventId", isSignal: true, isRequired: false, transformFunction: null }, templateId: { classPropertyName: "templateId", publicName: "templateId", isSignal: true, isRequired: false, transformFunction: null }, alertId: { classPropertyName: "alertId", publicName: "alertId", isSignal: true, isRequired: false, transformFunction: null }, sourceLabel: { classPropertyName: "sourceLabel", publicName: "sourceLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholderKeys: { classPropertyName: "placeholderKeys", publicName: "placeholderKeys", isSignal: true, isRequired: false, transformFunction: null }, placeholderFields: { classPropertyName: "placeholderFields", publicName: "placeholderFields", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery.user-test'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"real-email-warning-title\") }}</strong>\r\n {{ t(\"real-email-warning\") }}\r\n </div>\r\n\r\n @if (sourceLabel()) {\r\n <p class=\"mb-2 text-sm font-medium\">{{ sourceLabel() }}</p>\r\n }\r\n <p class=\"mb-4 text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}:\r\n <span class=\"break-all font-mono\">{{ testRequestId() }}</span>\r\n </p>\r\n\r\n @if (!result()) {\r\n <div class=\"grid gap-4\">\r\n <mt-textarea-field\r\n [formControl]=\"recipients\"\r\n [label]=\"t('recipients')\"\r\n [placeholder]=\"t('recipients-placeholder')\"\r\n [hint]=\"t('recipients-hint')\"\r\n rows=\"3\"\r\n [required]=\"true\"\r\n />\r\n\r\n @if (metadataControls().length > 0) {\r\n <div class=\"rounded-md border border-surface-200 p-4\">\r\n <p class=\"m-0 text-sm font-semibold\">\r\n {{ t(\"placeholder-values\") }}\r\n </p>\r\n <p class=\"mt-1 mb-4 text-xs text-muted-color\">\r\n {{ t(\"placeholder-values-hint\") }}\r\n </p>\r\n <div class=\"grid gap-4 md:grid-cols-2\">\r\n @for (item of metadataControls(); track item.key) {\r\n <mt-text-field\r\n [formControl]=\"item.control\"\r\n [label]=\"item.label\"\r\n [placeholder]=\"t('placeholder-value')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"no-placeholders\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n\r\n @if (polling() && !result()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-surface-200 bg-surface-50 px-4 py-3\"\r\n aria-live=\"polite\"\r\n >\r\n <p class=\"m-0 font-semibold\">\r\n {{ t(\"pending-title\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"pending-description\") }}\r\n </p>\r\n </div>\r\n } @else if (timedOut() && !result()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"m-0 font-semibold text-orange-950\">\r\n {{ t(\"timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-orange-900\">\r\n {{ t(\"timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as dispatch) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <p class=\"m-0 font-semibold\">\r\n {{ dispatch.allAccepted ? t(\"accepted\") : t(\"not-accepted\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"request-state\") }}: {{ dispatch.requestState }}\r\n </p>\r\n @if (dispatch.expectedRecipientCount !== null) {\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"recipient-count\") }}: {{ dispatch.expectedRecipientCount }}\r\n </p>\r\n }\r\n @if (dispatch.allAccepted) {\r\n <p class=\"mt-3 mb-0 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n\r\n @for (item of dispatch.results; track $index) {\r\n <div\r\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"item.outcome\" />\r\n <p class=\"mt-1 mb-0 text-xs text-muted-color\">\r\n {{ facade.describeCode(item.code) }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if ((polling() || timedOut()) && !result()) {\r\n <mt-button\r\n [label]=\"t('retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"\r\n polling() || facade.mutationLoading() === 'retrieve-user-test'\r\n \"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"\r\n facade.mutationLoading() === 'template-test' ||\r\n facade.mutationLoading() === 'alert-test'\r\n \"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2667
+ }
2668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailUserTestDialog, decorators: [{
2669
+ type: Component,
2670
+ args: [{ selector: 'mt-email-user-test-dialog', standalone: true, imports: [
2671
+ ReactiveFormsModule,
2672
+ TranslocoDirective,
2673
+ Button,
2674
+ TextareaField,
2675
+ TextField,
2676
+ EmailDeliveryStatus,
2677
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery.user-test'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"real-email-warning-title\") }}</strong>\r\n {{ t(\"real-email-warning\") }}\r\n </div>\r\n\r\n @if (sourceLabel()) {\r\n <p class=\"mb-2 text-sm font-medium\">{{ sourceLabel() }}</p>\r\n }\r\n <p class=\"mb-4 text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}:\r\n <span class=\"break-all font-mono\">{{ testRequestId() }}</span>\r\n </p>\r\n\r\n @if (!result()) {\r\n <div class=\"grid gap-4\">\r\n <mt-textarea-field\r\n [formControl]=\"recipients\"\r\n [label]=\"t('recipients')\"\r\n [placeholder]=\"t('recipients-placeholder')\"\r\n [hint]=\"t('recipients-hint')\"\r\n rows=\"3\"\r\n [required]=\"true\"\r\n />\r\n\r\n @if (metadataControls().length > 0) {\r\n <div class=\"rounded-md border border-surface-200 p-4\">\r\n <p class=\"m-0 text-sm font-semibold\">\r\n {{ t(\"placeholder-values\") }}\r\n </p>\r\n <p class=\"mt-1 mb-4 text-xs text-muted-color\">\r\n {{ t(\"placeholder-values-hint\") }}\r\n </p>\r\n <div class=\"grid gap-4 md:grid-cols-2\">\r\n @for (item of metadataControls(); track item.key) {\r\n <mt-text-field\r\n [formControl]=\"item.control\"\r\n [label]=\"item.label\"\r\n [placeholder]=\"t('placeholder-value')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"no-placeholders\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n\r\n @if (polling() && !result()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-surface-200 bg-surface-50 px-4 py-3\"\r\n aria-live=\"polite\"\r\n >\r\n <p class=\"m-0 font-semibold\">\r\n {{ t(\"pending-title\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"pending-description\") }}\r\n </p>\r\n </div>\r\n } @else if (timedOut() && !result()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"m-0 font-semibold text-orange-950\">\r\n {{ t(\"timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-orange-900\">\r\n {{ t(\"timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as dispatch) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <p class=\"m-0 font-semibold\">\r\n {{ dispatch.allAccepted ? t(\"accepted\") : t(\"not-accepted\") }}\r\n </p>\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"request-state\") }}: {{ dispatch.requestState }}\r\n </p>\r\n @if (dispatch.expectedRecipientCount !== null) {\r\n <p class=\"mt-1 mb-0 text-sm text-muted-color\">\r\n {{ t(\"recipient-count\") }}: {{ dispatch.expectedRecipientCount }}\r\n </p>\r\n }\r\n @if (dispatch.allAccepted) {\r\n <p class=\"mt-3 mb-0 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n\r\n @for (item of dispatch.results; track $index) {\r\n <div\r\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"item.outcome\" />\r\n <p class=\"mt-1 mb-0 text-xs text-muted-color\">\r\n {{ facade.describeCode(item.code) }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if ((polling() || timedOut()) && !result()) {\r\n <mt-button\r\n [label]=\"t('retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"\r\n polling() || facade.mutationLoading() === 'retrieve-user-test'\r\n \"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"\r\n facade.mutationLoading() === 'template-test' ||\r\n facade.mutationLoading() === 'alert-test'\r\n \"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n" }]
2678
+ }], ctorParameters: () => [], propDecorators: { kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: true }] }], eventId: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventId", required: false }] }], templateId: [{ type: i0.Input, args: [{ isSignal: true, alias: "templateId", required: false }] }], alertId: [{ type: i0.Input, args: [{ isSignal: true, alias: "alertId", required: false }] }], sourceLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceLabel", required: false }] }], placeholderKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderKeys", required: false }] }], placeholderFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderFields", required: false }] }] } });
2679
+ const POLL_DELAYS_MS = [1_000, 2_000, 4_000, 5_000, 5_000, 5_000];
2680
+ const TEST_REQUEST_CONFLICT_CODE = 'email.delivery_test_request_conflict';
2681
+ function isAmbiguousTimeout(error) {
2682
+ return (error instanceof HttpErrorResponse &&
2683
+ (error.status === 0 || error.status === 408 || error.status === 504));
2684
+ }
2685
+ function isTestRequestConflict(error) {
2686
+ const body = error instanceof HttpErrorResponse ? error.error : error;
2687
+ if (typeof body !== 'object' || body === null)
2688
+ return false;
2689
+ const value = body;
2690
+ return (value.code === TEST_REQUEST_CONFLICT_CODE ||
2691
+ value.errors?.code === TEST_REQUEST_CONFLICT_CODE);
2692
+ }
2693
+
2694
+ const EMAIL_DELIVERY_ROUTES = [
2695
+ {
2696
+ path: '',
2697
+ loadComponent: () => import('./masterteam-system-settings-email-delivery-shell-Bjwtj5Ic.mjs').then((module) => module.EmailDeliveryShell),
2698
+ },
2699
+ { path: '**', redirectTo: '' },
2700
+ ];
2701
+
2702
+ /**
2703
+ * Generated bundle index. Do not edit.
2704
+ */
2705
+
2706
+ export { DEFAULT_GENERAL_CONFIGURATION as D, EmailDeliveryFacade as E, FIXED_CURRENCIES as F, GENERAL_CONFIGURATION_SETTING_KEY as G, RATE_SOURCE_OPTIONS as R, SystemSettings as S, TIMESHEET_FREQUENCY_OPTIONS as T, WEEKEND_DAY_DEFINITIONS as W, EmailDeliveryStatus as a, EMAIL_DELIVERY_ATTEMPT_STATUSES as b, createEmailTestRequestId as c, areEmailTestRecipientsValid as d, isEmailTestRequestPending as e, EMAIL_DELIVERY_PERMISSIONS as f, getEmailDeliveryStatusMap as g, EXPLORE_DEFAULT_VIEW_OPTIONS as h, isConnectionTestAttempt as i, DEFAULT_DATE_FORMAT_OPTIONS as j, DEFAULT_TIME_FORMAT_OPTIONS as k, DOTNET_DAY_DEFINITIONS as l, SCHEDULE_ITEM_TYPE_OPTIONS as m, SystemSettingsHolidayEditor as n, DEFAULT_EMAIL_DELIVERY_OPTIONS as o, parseEmailTestRecipients as p, EMAIL_DELIVERY_OPTIONS as q, provideEmailDelivery as r, EmailDeliveryAccessService as s, collectEmailTemplatePlaceholders as t, buildEmailTemplatePlaceholderFields as u, EmailUserTestDialog as v, EMAIL_USER_INITIATED_TEST_OPERATION_KIND as w, EMAIL_DELIVERY_ROUTES as x };
2707
+ //# sourceMappingURL=masterteam-system-settings-masterteam-system-settings-CO-XTJOi.mjs.map