@idea-ionic/uncommon 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +21 -0
  3. package/css/global.scss +15 -0
  4. package/esm2022/idea-ionic-uncommon.mjs +5 -0
  5. package/esm2022/public-api.mjs +35 -0
  6. package/esm2022/src/AWSAPI.service.mjs +430 -0
  7. package/esm2022/src/AWSAPISocket.service.mjs +187 -0
  8. package/esm2022/src/address/address.component.mjs +94 -0
  9. package/esm2022/src/address/address.module.mjs +21 -0
  10. package/esm2022/src/announcement/announcement.component.mjs +41 -0
  11. package/esm2022/src/announcement/announcement.module.mjs +21 -0
  12. package/esm2022/src/cachedResources.service.mjs +55 -0
  13. package/esm2022/src/colors/colorPicker.component.mjs +135 -0
  14. package/esm2022/src/colors/colorPicker.module.mjs +20 -0
  15. package/esm2022/src/contacts/contacts.component.mjs +64 -0
  16. package/esm2022/src/contacts/contacts.module.mjs +21 -0
  17. package/esm2022/src/dateTime/calendarPicker.component.mjs +196 -0
  18. package/esm2022/src/dateTime/dateTime.component.mjs +133 -0
  19. package/esm2022/src/dateTime/dateTime.module.mjs +22 -0
  20. package/esm2022/src/duration/duration.component.mjs +77 -0
  21. package/esm2022/src/duration/duration.module.mjs +21 -0
  22. package/esm2022/src/echo/echo.module.mjs +26 -0
  23. package/esm2022/src/echo/echo.page.mjs +165 -0
  24. package/esm2022/src/errorReporting.service.mjs +68 -0
  25. package/esm2022/src/mde/mde.component.mjs +44 -0
  26. package/esm2022/src/mde/mde.module.mjs +22 -0
  27. package/esm2022/src/mde/mdeToolbar.component.mjs +12 -0
  28. package/esm2022/src/offline/cacheableResource.component.mjs +18 -0
  29. package/esm2022/src/offline/offline.module.mjs +22 -0
  30. package/esm2022/src/offline/offline.service.mjs +62 -0
  31. package/esm2022/src/offline/offlineData.service.mjs +479 -0
  32. package/esm2022/src/offline/offlineIndicator.component.mjs +58 -0
  33. package/esm2022/src/offline/offlineManager.component.mjs +84 -0
  34. package/esm2022/src/pushNotifications.service.mjs +56 -0
  35. package/esm2022/src/sentiment/sentiment.component.mjs +71 -0
  36. package/esm2022/src/sentiment/sentiment.module.mjs +21 -0
  37. package/esm2022/src/timeInterval/fromTimeToTime.component.mjs +155 -0
  38. package/esm2022/src/timeInterval/timeInterval.component.mjs +122 -0
  39. package/esm2022/src/timeInterval/timeInterval.module.mjs +22 -0
  40. package/esm2022/src/tinCan.service.mjs +49 -0
  41. package/fesm2022/idea-ionic-uncommon.mjs +2891 -0
  42. package/fesm2022/idea-ionic-uncommon.mjs.map +1 -0
  43. package/index.d.ts +5 -0
  44. package/package.json +51 -0
  45. package/public-api.d.ts +34 -0
  46. package/src/AWSAPI.service.d.ts +178 -0
  47. package/src/AWSAPISocket.service.d.ts +129 -0
  48. package/src/address/address.component.d.ts +64 -0
  49. package/src/address/address.module.d.ts +11 -0
  50. package/src/announcement/announcement.component.d.ts +23 -0
  51. package/src/announcement/announcement.module.d.ts +11 -0
  52. package/src/cachedResources.service.d.ts +34 -0
  53. package/src/colors/colorPicker.component.d.ts +78 -0
  54. package/src/colors/colorPicker.module.d.ts +10 -0
  55. package/src/contacts/contacts.component.d.ts +31 -0
  56. package/src/contacts/contacts.module.d.ts +11 -0
  57. package/src/dateTime/calendarPicker.component.d.ts +45 -0
  58. package/src/dateTime/dateTime.component.d.ts +81 -0
  59. package/src/dateTime/dateTime.module.d.ts +12 -0
  60. package/src/duration/duration.component.d.ts +68 -0
  61. package/src/duration/duration.module.d.ts +11 -0
  62. package/src/echo/echo.module.d.ts +12 -0
  63. package/src/echo/echo.page.d.ts +34 -0
  64. package/src/errorReporting.service.d.ts +26 -0
  65. package/src/mde/mde.component.d.ts +39 -0
  66. package/src/mde/mde.module.d.ts +12 -0
  67. package/src/mde/mdeToolbar.component.d.ts +5 -0
  68. package/src/offline/cacheableResource.component.d.ts +11 -0
  69. package/src/offline/offline.module.d.ts +12 -0
  70. package/src/offline/offline.service.d.ts +39 -0
  71. package/src/offline/offlineData.service.d.ts +281 -0
  72. package/src/offline/offlineIndicator.component.d.ts +25 -0
  73. package/src/offline/offlineManager.component.d.ts +17 -0
  74. package/src/pushNotifications.service.d.ts +26 -0
  75. package/src/sentiment/sentiment.component.d.ts +35 -0
  76. package/src/sentiment/sentiment.module.d.ts +11 -0
  77. package/src/timeInterval/fromTimeToTime.component.d.ts +61 -0
  78. package/src/timeInterval/timeInterval.component.d.ts +84 -0
  79. package/src/timeInterval/timeInterval.module.d.ts +12 -0
  80. package/src/tinCan.service.d.ts +31 -0
@@ -0,0 +1,2891 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, Component, Input, Output, ViewChild, NgModule, Injectable, inject } from '@angular/core';
3
+ import { Countries, Suggestion, mdToHtml, COLORS, Contacts, ErrorReport, ClientInfo, ExternalCalendarSources, Sentiment, TimeInterval, PushNotificationsDevice, PushNotificationsPlatforms } from 'idea-toolbox';
4
+ import * as i1 from '@angular/forms';
5
+ import { FormsModule } from '@angular/forms';
6
+ import * as i1$1 from '@ionic/angular';
7
+ import { IonicModule, PopoverController, AlertController, ModalController, Platform } from '@ionic/angular';
8
+ import * as i3 from '@idea-ionic/common';
9
+ import { IDEATranslationsModule, IDEASelectModule, IDEAEnvironment, IDEAStorageService, IDEATranslationsService, IDEALoadingService, IDEAActionSheetController } from '@idea-ionic/common';
10
+ import * as i2 from '@angular/common';
11
+ import { CommonModule } from '@angular/common';
12
+ import * as i1$2 from '@angular/router';
13
+ import { ActivatedRoute, RouterModule } from '@angular/router';
14
+ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
15
+ import { Observable } from 'rxjs';
16
+ import { Network } from '@capacitor/network';
17
+ import { Browser } from '@capacitor/browser';
18
+ import { PushNotifications } from '@capacitor/push-notifications';
19
+
20
+ class IDEAAddressComponent {
21
+ constructor() {
22
+ this.addressChange = new EventEmitter();
23
+ /**
24
+ * If true, show the field `contact`.
25
+ */
26
+ this.showContact = false;
27
+ /**
28
+ * If true, show the field `address2`.
29
+ */
30
+ this.showAddress2 = false;
31
+ /**
32
+ * If true, show the field `phone`.
33
+ */
34
+ this.showPhone = false;
35
+ /**
36
+ * If true, show the field `email`.
37
+ */
38
+ this.showEmail = false;
39
+ /**
40
+ * Whether the fields are editable or disabled.
41
+ */
42
+ this.editMode = true;
43
+ /**
44
+ * If true, show obligatory dots.
45
+ */
46
+ this.obligatory = false;
47
+ /**
48
+ * To toggle the detailed view.
49
+ */
50
+ this.openByDefault = false;
51
+ this.Countries = Countries;
52
+ }
53
+ ngOnInit() {
54
+ this.countriesSuggestions = Object.keys(Countries).map(k => new Suggestion({ value: Countries[k], name: k }));
55
+ }
56
+ isCollapsed() {
57
+ return this.accordion?.value !== 'open';
58
+ }
59
+ getCountryName(countryCode) {
60
+ if (!countryCode)
61
+ return;
62
+ const country = Object.entries(Countries).find(([_, value]) => value === countryCode);
63
+ return country ? country[0] : null;
64
+ }
65
+ setCountryFromSuggestion(suggestion) {
66
+ if (!suggestion)
67
+ return;
68
+ this.address.country = suggestion.value;
69
+ this.addressChange.emit(this.address);
70
+ }
71
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAAddressComponent, selector: "idea-address", inputs: { address: "address", showContact: "showContact", showAddress2: "showAddress2", showPhone: "showPhone", showEmail: "showEmail", editMode: "editMode", obligatory: "obligatory", lines: "lines", color: "color", label: "label", placeholder: "placeholder", openByDefault: "openByDefault" }, outputs: { addressChange: "addressChange" }, viewQueries: [{ propertyName: "accordion", first: true, predicate: ["accordion"], descendants: true }], ngImport: i0, template: "@if (address) {\n <ion-accordion-group #accordion>\n <ion-accordion value=\"open\">\n <ion-item slot=\"header\" button [color]=\"color\" [lines]=\"lines\">\n @if (isCollapsed()) {\n <ion-input\n readonly\n [labelPlacement]=\"isCollapsed() ? 'stacked' : ''\"\n [label]=\"label || ('IDEA_UNCOMMON.ADDRESS.COMPLETE_ADDRESS' | translate)\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.COMPLETE_ADDRESS_HINT' | translate\"\n [placeholder]=\"placeholder\"\n [value]=\"address.getFullAddress()\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n }\n </ion-item>\n <ion-list slot=\"content\">\n @if (showContact) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT_HINT' | translate\"\n [(ngModel)]=\"address.contact\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n @if (showEmail) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL_HINT' | translate\"\n [(ngModel)]=\"address.email\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n @if (showPhone) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.PHONE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.PHONE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.PHONE_HINT' | translate\"\n [(ngModel)]=\"address.phone\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS_HINT' | translate\"\n [(ngModel)]=\"address.address\"\n (ionChange)=\"addressChange.emit(address)\"\n >\n <div slot=\"label\">\n {{ 'IDEA_UNCOMMON.ADDRESS.ADDRESS' | translate }}\n @if (editMode && obligatory) {\n <ion-text class=\"obligatoryDot\" />\n }\n </div>\n </ion-input>\n </ion-item>\n @if (showAddress2) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2_HINT' | translate\"\n [(ngModel)]=\"address.address2\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE_HINT' | translate\"\n [(ngModel)]=\"address.postcode\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n [disabled]=\"!editMode\"\n labelPlacement=\"stacked\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.CITY_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.CITY_HINT' | translate\"\n [(ngModel)]=\"address.city\"\n (ionChange)=\"addressChange.emit(address)\"\n >\n <div slot=\"label\">\n {{ 'IDEA_UNCOMMON.ADDRESS.CITY' | translate }}\n @if (editMode && obligatory) {\n <ion-text class=\"obligatoryDot\" />\n }\n </div>\n </ion-input>\n </ion-item>\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE_HINT' | translate\"\n [(ngModel)]=\"address.province\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n <idea-select\n [color]=\"color\"\n [data]=\"countriesSuggestions\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY' | translate\"\n [lines]=\"editMode ? lines : null\"\n [description]=\"getCountryName(address.country)\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY_HINT' | translate\"\n [searchPlaceholder]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY_HINT' | translate\"\n [hideIdFromUI]=\"true\"\n [avoidAutoSelection]=\"true\"\n [disabled]=\"!editMode\"\n (select)=\"setCountryFromSuggestion($event)\"\n />\n </ion-list>\n </ion-accordion>\n </ion-accordion-group>\n}\n", styles: ["ion-item[slot=header] ion-input{--highlight-color-focused: transparent;--highlight-color-valid: transparent;--highlight-color-invalid: transparent}ion-list[slot=content]{padding-top:0;padding-left:20px}\n"], dependencies: [{ kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1$1.IonAccordion, selector: "ion-accordion", inputs: ["disabled", "mode", "readonly", "toggleIcon", "toggleIconSlot", "value"] }, { kind: "component", type: i1$1.IonAccordionGroup, selector: "ion-accordion-group", inputs: ["animated", "disabled", "expand", "mode", "multiple", "readonly", "value"] }, { kind: "component", type: i1$1.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TextValueAccessor, selector: "ion-input:not([type=number]),ion-textarea,ion-searchbar" }, { kind: "component", type: i3.IDEASelectComponent, selector: "idea-select", inputs: ["description", "data", "dataProvider", "label", "icon", "iconColor", "placeholder", "searchPlaceholder", "noElementsFoundText", "disabled", "tappableWhenDisabled", "obligatory", "lines", "color", "allowUnlistedValues", "allowUnlistedValuesPrefix", "sortData", "clearValueAfterSelection", "hideIdFromUI", "hideClearButton", "mustChoose", "category1", "category2", "showCategoriesFilters", "avoidAutoSelection"], outputs: ["select", "iconSelect", "selectWhenDisabled"] }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
73
+ }
74
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressComponent, decorators: [{
75
+ type: Component,
76
+ args: [{ selector: 'idea-address', template: "@if (address) {\n <ion-accordion-group #accordion>\n <ion-accordion value=\"open\">\n <ion-item slot=\"header\" button [color]=\"color\" [lines]=\"lines\">\n @if (isCollapsed()) {\n <ion-input\n readonly\n [labelPlacement]=\"isCollapsed() ? 'stacked' : ''\"\n [label]=\"label || ('IDEA_UNCOMMON.ADDRESS.COMPLETE_ADDRESS' | translate)\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.COMPLETE_ADDRESS_HINT' | translate\"\n [placeholder]=\"placeholder\"\n [value]=\"address.getFullAddress()\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n }\n </ion-item>\n <ion-list slot=\"content\">\n @if (showContact) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.CONTACT_HINT' | translate\"\n [(ngModel)]=\"address.contact\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n @if (showEmail) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.EMAIL_HINT' | translate\"\n [(ngModel)]=\"address.email\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n @if (showPhone) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.PHONE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.PHONE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.PHONE_HINT' | translate\"\n [(ngModel)]=\"address.phone\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS_HINT' | translate\"\n [(ngModel)]=\"address.address\"\n (ionChange)=\"addressChange.emit(address)\"\n >\n <div slot=\"label\">\n {{ 'IDEA_UNCOMMON.ADDRESS.ADDRESS' | translate }}\n @if (editMode && obligatory) {\n <ion-text class=\"obligatoryDot\" />\n }\n </div>\n </ion-input>\n </ion-item>\n @if (showAddress2) {\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.ADDRESS2_HINT' | translate\"\n [(ngModel)]=\"address.address2\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n }\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.POSTCODE_HINT' | translate\"\n [(ngModel)]=\"address.postcode\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n [disabled]=\"!editMode\"\n labelPlacement=\"stacked\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.CITY_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.CITY_HINT' | translate\"\n [(ngModel)]=\"address.city\"\n (ionChange)=\"addressChange.emit(address)\"\n >\n <div slot=\"label\">\n {{ 'IDEA_UNCOMMON.ADDRESS.CITY' | translate }}\n @if (editMode && obligatory) {\n <ion-text class=\"obligatoryDot\" />\n }\n </div>\n </ion-input>\n </ion-item>\n <ion-item [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.ADDRESS.PROVINCE_HINT' | translate\"\n [(ngModel)]=\"address.province\"\n (ionChange)=\"addressChange.emit(address)\"\n />\n </ion-item>\n <idea-select\n [color]=\"color\"\n [data]=\"countriesSuggestions\"\n [label]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY' | translate\"\n [lines]=\"editMode ? lines : null\"\n [description]=\"getCountryName(address.country)\"\n [placeholder]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY_HINT' | translate\"\n [searchPlaceholder]=\"'IDEA_UNCOMMON.ADDRESS.COUNTRY_HINT' | translate\"\n [hideIdFromUI]=\"true\"\n [avoidAutoSelection]=\"true\"\n [disabled]=\"!editMode\"\n (select)=\"setCountryFromSuggestion($event)\"\n />\n </ion-list>\n </ion-accordion>\n </ion-accordion-group>\n}\n", styles: ["ion-item[slot=header] ion-input{--highlight-color-focused: transparent;--highlight-color-valid: transparent;--highlight-color-invalid: transparent}ion-list[slot=content]{padding-top:0;padding-left:20px}\n"] }]
77
+ }], propDecorators: { address: [{
78
+ type: Input
79
+ }], addressChange: [{
80
+ type: Output
81
+ }], showContact: [{
82
+ type: Input
83
+ }], showAddress2: [{
84
+ type: Input
85
+ }], showPhone: [{
86
+ type: Input
87
+ }], showEmail: [{
88
+ type: Input
89
+ }], editMode: [{
90
+ type: Input
91
+ }], obligatory: [{
92
+ type: Input
93
+ }], lines: [{
94
+ type: Input
95
+ }], color: [{
96
+ type: Input
97
+ }], label: [{
98
+ type: Input
99
+ }], placeholder: [{
100
+ type: Input
101
+ }], openByDefault: [{
102
+ type: Input
103
+ }], accordion: [{
104
+ type: ViewChild,
105
+ args: ['accordion']
106
+ }] } });
107
+
108
+ class IDEAAddressModule {
109
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
110
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressModule, declarations: [IDEAAddressComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule], exports: [IDEAAddressComponent] }); }
111
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule] }); }
112
+ }
113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAddressModule, decorators: [{
114
+ type: NgModule,
115
+ args: [{
116
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule],
117
+ declarations: [IDEAAddressComponent],
118
+ exports: [IDEAAddressComponent]
119
+ }]
120
+ }] });
121
+
122
+ /**
123
+ * @deprecated use a specific App service instead.
124
+ */
125
+ class IDEATinCanService {
126
+ constructor() {
127
+ this.clear();
128
+ }
129
+ /**
130
+ * Set a variable with the given value.
131
+ * @param variable variable to set
132
+ * @param value value to assign to the variable
133
+ */
134
+ set(variable, value) {
135
+ this.data[variable] = value;
136
+ }
137
+ /**
138
+ * Get the content of a variable.
139
+ * @param variable variable to acquire
140
+ * @param getAndDelete if set, delete the variable after the acquisition
141
+ */
142
+ get(variable, getAndDelete) {
143
+ const ret = this.data[variable];
144
+ if (getAndDelete)
145
+ this.remove(variable);
146
+ return ret;
147
+ }
148
+ /**
149
+ * Remove a variable.
150
+ * @param variable variable to remove
151
+ */
152
+ remove(variable) {
153
+ delete this.data[variable];
154
+ }
155
+ /**
156
+ * Clear all the variables.
157
+ */
158
+ clear() {
159
+ this.data = {};
160
+ }
161
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATinCanService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
162
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATinCanService, providedIn: 'root' }); }
163
+ }
164
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATinCanService, decorators: [{
165
+ type: Injectable,
166
+ args: [{ providedIn: 'root' }]
167
+ }], ctorParameters: () => [] });
168
+
169
+ /**
170
+ * Announcement card that can be set in any interface to show the alert for this project.
171
+ */
172
+ class IDEAAnnouncementComponent {
173
+ constructor() {
174
+ this._env = inject(IDEAEnvironment);
175
+ this._tc = inject(IDEATinCanService);
176
+ this._storage = inject(IDEAStorageService);
177
+ this.storageKey = (this._env.idea.project || 'app').concat('_LAST_ANNOUNCEMENT');
178
+ this.color = 'dark';
179
+ }
180
+ async ngOnInit() {
181
+ // standard IDEA: the announcement is read in the Init Guard
182
+ this.announcement = this._tc.get('idea-announcement');
183
+ if (this.announcement && this.announcement.content) {
184
+ const lastRead = await this._storage.get(this.storageKey);
185
+ if (!lastRead || this.announcement.content !== lastRead)
186
+ this.htmlContent = mdToHtml(this.announcement.content);
187
+ }
188
+ }
189
+ async markAsRead() {
190
+ await this._storage.set(this.storageKey, this.announcement.content);
191
+ this.htmlContent = null;
192
+ }
193
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
194
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAAnnouncementComponent, selector: "idea-announcement", inputs: { color: "color" }, ngImport: i0, template: "@if (htmlContent) {\n <ion-card class=\"announcementCard\" [color]=\"color\">\n <ion-card-content>\n <p class=\"htmlContent\" [innerHTML]=\"htmlContent\"></p>\n <p class=\"ion-text-right\">\n <ion-button color=\"primary\" (click)=\"markAsRead()\">\n {{ 'COMMON.GOT_IT' | translate }}\n </ion-button>\n </p>\n </ion-card-content>\n </ion-card>\n}\n", styles: [""], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
195
+ }
196
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementComponent, decorators: [{
197
+ type: Component,
198
+ args: [{ selector: 'idea-announcement', template: "@if (htmlContent) {\n <ion-card class=\"announcementCard\" [color]=\"color\">\n <ion-card-content>\n <p class=\"htmlContent\" [innerHTML]=\"htmlContent\"></p>\n <p class=\"ion-text-right\">\n <ion-button color=\"primary\" (click)=\"markAsRead()\">\n {{ 'COMMON.GOT_IT' | translate }}\n </ion-button>\n </p>\n </ion-card-content>\n </ion-card>\n}\n" }]
199
+ }], ctorParameters: () => [], propDecorators: { color: [{
200
+ type: Input
201
+ }] } });
202
+
203
+ class IDEAAnnouncementModule {
204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
205
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementModule, declarations: [IDEAAnnouncementComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEAAnnouncementComponent] }); }
206
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
207
+ }
208
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAnnouncementModule, decorators: [{
209
+ type: NgModule,
210
+ args: [{
211
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
212
+ declarations: [IDEAAnnouncementComponent],
213
+ exports: [IDEAAnnouncementComponent]
214
+ }]
215
+ }] });
216
+
217
+ /**
218
+ * Pick a color from a defined set.
219
+ */
220
+ class IDEAColorPickerComponent {
221
+ constructor() {
222
+ /**
223
+ * The pickable colors.
224
+ */
225
+ this.colors = COLORS;
226
+ /**
227
+ * On select event.
228
+ */
229
+ this.select = new EventEmitter();
230
+ /**
231
+ * Icon select.
232
+ */
233
+ this.iconSelect = new EventEmitter();
234
+ this._popover = inject(PopoverController);
235
+ }
236
+ async openPalette(event) {
237
+ if (this.disabled)
238
+ return;
239
+ const componentProps = { colors: this.colors, current: this.current };
240
+ const popover = await this._popover.create({ component: ColorsPaletteComponent, componentProps, event });
241
+ popover.onDidDismiss().then(res => {
242
+ if (res && res.data)
243
+ this.select.emit(res.data);
244
+ });
245
+ popover.present();
246
+ }
247
+ doIconSelect(event) {
248
+ if (event)
249
+ event.stopPropagation();
250
+ this.iconSelect.emit(event);
251
+ }
252
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
253
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAColorPickerComponent, selector: "idea-color-picker", inputs: { colors: "colors", current: "current", label: "label", placeholder: "placeholder", icon: "icon", iconColor: "iconColor", disabled: "disabled", obligatory: "obligatory", lines: "lines", color: "color" }, outputs: { select: "select", iconSelect: "iconSelect" }, ngImport: i0, template: "<ion-item\n class=\"colorPickerItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [button]=\"!disabled\"\n [title]=\"placeholder || ''\"\n [class.withLabel]=\"label\"\n (click)=\"openPalette($event)\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" (click)=\"doIconSelect($event)\">\n <ion-icon [name]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label [class.selectable]=\"!disabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-avatar slot=\"end\" class=\"colorCircle\" [style.background-color]=\"current\" />\n @if (!disabled) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".colorPickerItem{min-height:48px;height:auto}.colorPickerItem .placeholder{color:var(--ion-color-medium)}.colorPickerItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectItem.withLabel{min-height:58px;height:auto}.selectItem.withLabel .selectIcon{padding-top:25px}.selectItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}ion-avatar.colorCircle{width:20px;height:20px}\n"], dependencies: [{ kind: "component", type: i1$1.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }] }); }
254
+ }
255
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerComponent, decorators: [{
256
+ type: Component,
257
+ args: [{ selector: 'idea-color-picker', template: "<ion-item\n class=\"colorPickerItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [button]=\"!disabled\"\n [title]=\"placeholder || ''\"\n [class.withLabel]=\"label\"\n (click)=\"openPalette($event)\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" (click)=\"doIconSelect($event)\">\n <ion-icon [name]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label [class.selectable]=\"!disabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-avatar slot=\"end\" class=\"colorCircle\" [style.background-color]=\"current\" />\n @if (!disabled) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".colorPickerItem{min-height:48px;height:auto}.colorPickerItem .placeholder{color:var(--ion-color-medium)}.colorPickerItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectItem.withLabel{min-height:58px;height:auto}.selectItem.withLabel .selectIcon{padding-top:25px}.selectItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}ion-avatar.colorCircle{width:20px;height:20px}\n"] }]
258
+ }], propDecorators: { colors: [{
259
+ type: Input
260
+ }], current: [{
261
+ type: Input
262
+ }], label: [{
263
+ type: Input
264
+ }], placeholder: [{
265
+ type: Input
266
+ }], icon: [{
267
+ type: Input
268
+ }], iconColor: [{
269
+ type: Input
270
+ }], disabled: [{
271
+ type: Input
272
+ }], obligatory: [{
273
+ type: Input
274
+ }], lines: [{
275
+ type: Input
276
+ }], color: [{
277
+ type: Input
278
+ }], select: [{
279
+ type: Output
280
+ }], iconSelect: [{
281
+ type: Output
282
+ }] } });
283
+ /**
284
+ * Component to open in a popover for displaying the colors available.
285
+ */
286
+ class ColorsPaletteComponent {
287
+ constructor() {
288
+ this._popover = inject(PopoverController);
289
+ }
290
+ pick(color) {
291
+ this._popover.dismiss(color);
292
+ }
293
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: ColorsPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
294
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: ColorsPaletteComponent, selector: "idea-colors-palette", inputs: { colors: "colors", current: "current" }, ngImport: i0, template: `
295
+ <ion-content>
296
+ <ion-grid>
297
+ <ion-row>
298
+ @for (c of colors; track c) {
299
+ <ion-col [size]="2">
300
+ <ion-avatar
301
+ class="colorCircle tappable"
302
+ [style.background-color]="c.hex"
303
+ [title]="c.name || c.hex"
304
+ (click)="pick(c.hex)"
305
+ >
306
+ @if (c.hex === current) {
307
+ <ion-icon name="checkmark" />
308
+ }
309
+ </ion-avatar>
310
+ </ion-col>
311
+ }
312
+ </ion-row>
313
+ </ion-grid>
314
+ </ion-content>
315
+ `, isInline: true, styles: ["ion-content{--background: var(--ion-color-white)}ion-avatar.colorCircle{width:20px;height:20px}.tappable{cursor:pointer}ion-avatar.colorCircle ion-icon{color:#fff;font-size:1.2em}\n"], dependencies: [{ kind: "component", type: i1$1.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i1$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonRow, selector: "ion-row" }] }); }
316
+ }
317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: ColorsPaletteComponent, decorators: [{
318
+ type: Component,
319
+ args: [{ selector: 'idea-colors-palette', template: `
320
+ <ion-content>
321
+ <ion-grid>
322
+ <ion-row>
323
+ @for (c of colors; track c) {
324
+ <ion-col [size]="2">
325
+ <ion-avatar
326
+ class="colorCircle tappable"
327
+ [style.background-color]="c.hex"
328
+ [title]="c.name || c.hex"
329
+ (click)="pick(c.hex)"
330
+ >
331
+ @if (c.hex === current) {
332
+ <ion-icon name="checkmark" />
333
+ }
334
+ </ion-avatar>
335
+ </ion-col>
336
+ }
337
+ </ion-row>
338
+ </ion-grid>
339
+ </ion-content>
340
+ `, styles: ["ion-content{--background: var(--ion-color-white)}ion-avatar.colorCircle{width:20px;height:20px}.tappable{cursor:pointer}ion-avatar.colorCircle ion-icon{color:#fff;font-size:1.2em}\n"] }]
341
+ }], propDecorators: { colors: [{
342
+ type: Input
343
+ }], current: [{
344
+ type: Input
345
+ }] } });
346
+
347
+ class IDEAColorPickerModule {
348
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
349
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerModule, declarations: [IDEAColorPickerComponent, ColorsPaletteComponent], imports: [CommonModule, FormsModule, IonicModule], exports: [IDEAColorPickerComponent, ColorsPaletteComponent] }); }
350
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerModule, imports: [CommonModule, FormsModule, IonicModule] }); }
351
+ }
352
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAColorPickerModule, decorators: [{
353
+ type: NgModule,
354
+ args: [{
355
+ imports: [CommonModule, FormsModule, IonicModule],
356
+ declarations: [IDEAColorPickerComponent, ColorsPaletteComponent],
357
+ exports: [IDEAColorPickerComponent, ColorsPaletteComponent]
358
+ }]
359
+ }] });
360
+
361
+ class IDEAContactsComponent {
362
+ constructor() {
363
+ this._alert = inject(AlertController);
364
+ this._translate = inject(IDEATranslationsService);
365
+ /**
366
+ * The contacts to manage.
367
+ */
368
+ this.contacts = new Contacts();
369
+ /**
370
+ * If true, show the field `name`.
371
+ */
372
+ this.showName = false;
373
+ /**
374
+ * Whether the fields are editable or disabled.
375
+ */
376
+ this.editMode = true;
377
+ }
378
+ sendEmail() {
379
+ if (!this.contacts.email)
380
+ return;
381
+ const url = `mailto:${this.contacts.email}`;
382
+ this.preExternalAction(this.contacts.email, () => window.open(url, '_system'));
383
+ }
384
+ call() {
385
+ if (!this.contacts.phone)
386
+ return;
387
+ const url = `tel:${this.contacts.phone}`;
388
+ this.preExternalAction(this.contacts.phone, () => window.open(url, '_system'));
389
+ }
390
+ preExternalAction(message, cb) {
391
+ const header = this._translate._('COMMON.DO_YOU_WANT_TO_PROCEED');
392
+ const buttons = [
393
+ { text: this._translate._('COMMON.CANCEL') },
394
+ { text: this._translate._('COMMON.CONFIRM'), handler: () => cb() }
395
+ ];
396
+ const cssClass = 'selectableAlertMessage';
397
+ this._alert.create({ header, message, buttons, cssClass }).then(alert => alert.present());
398
+ }
399
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
400
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAContactsComponent, selector: "idea-contacts", inputs: { contacts: "contacts", showName: "showName", editMode: "editMode", lines: "lines", color: "color" }, ngImport: i0, template: "<div class=\"contacts\">\n @if (showName) {\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.NAME' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate\"\n [(ngModel)]=\"contacts.name\"\n />\n </ion-item>\n }\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.PHONE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate\"\n [(ngModel)]=\"contacts.phone\"\n />\n @if (!editMode) {\n <ion-button\n slot=\"end\"\n fill=\"clear\"\n color=\"dark\"\n class=\"marginTop\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.CALL' | translate\"\n (click)=\"call()\"\n >\n <ion-icon name=\"call\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate\"\n [(ngModel)]=\"contacts.email\"\n />\n @if (!editMode) {\n <ion-button\n slot=\"end\"\n fill=\"clear\"\n color=\"dark\"\n class=\"marginTop\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate\"\n (click)=\"sendEmail()\"\n >\n <ion-icon name=\"mail\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n</div>\n", styles: [".marginTop{margin-top:14px}\n"], dependencies: [{ kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "directive", type: i1$1.TextValueAccessor, selector: "ion-input:not([type=number]),ion-textarea,ion-searchbar" }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
401
+ }
402
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsComponent, decorators: [{
403
+ type: Component,
404
+ args: [{ selector: 'idea-contacts', template: "<div class=\"contacts\">\n @if (showName) {\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.NAME' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate\"\n [(ngModel)]=\"contacts.name\"\n />\n </ion-item>\n }\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.PHONE' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate\"\n [(ngModel)]=\"contacts.phone\"\n />\n @if (!editMode) {\n <ion-button\n slot=\"end\"\n fill=\"clear\"\n color=\"dark\"\n class=\"marginTop\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.CALL' | translate\"\n (click)=\"call()\"\n >\n <ion-icon name=\"call\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n <ion-item [lines]=\"lines\" [color]=\"color\">\n <ion-input\n autocomplete=\"new\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate\"\n [disabled]=\"!editMode\"\n [placeholder]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate\"\n [(ngModel)]=\"contacts.email\"\n />\n @if (!editMode) {\n <ion-button\n slot=\"end\"\n fill=\"clear\"\n color=\"dark\"\n class=\"marginTop\"\n [title]=\"'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate\"\n (click)=\"sendEmail()\"\n >\n <ion-icon name=\"mail\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n</div>\n", styles: [".marginTop{margin-top:14px}\n"] }]
405
+ }], propDecorators: { contacts: [{
406
+ type: Input
407
+ }], showName: [{
408
+ type: Input
409
+ }], editMode: [{
410
+ type: Input
411
+ }], lines: [{
412
+ type: Input
413
+ }], color: [{
414
+ type: Input
415
+ }] } });
416
+
417
+ class IDEAContactsModule {
418
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
419
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsModule, declarations: [IDEAContactsComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule], exports: [IDEAContactsComponent] }); }
420
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule] }); }
421
+ }
422
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAContactsModule, decorators: [{
423
+ type: NgModule,
424
+ args: [{
425
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, IDEASelectModule],
426
+ declarations: [IDEAContactsComponent],
427
+ exports: [IDEAContactsComponent]
428
+ }]
429
+ }] });
430
+
431
+ /**
432
+ * A random sunday, used as reference to calculate the week days.
433
+ */
434
+ const SUNDAY = 259200000;
435
+ class IDEACalendarPickerComponent {
436
+ constructor() {
437
+ this._modal = inject(ModalController);
438
+ this._alert = inject(AlertController);
439
+ this._translate = inject(IDEATranslationsService);
440
+ this.timePicker = false;
441
+ this.manualTimePicker = false;
442
+ this.hideClearButton = false;
443
+ }
444
+ ngOnInit() {
445
+ this.today = new Date();
446
+ this.refDate = this.inputDate ? new Date(this.inputDate) : new Date(this.today);
447
+ this.selectedDate = new Date(this.refDate);
448
+ this.hour = 12; // to endure timezones
449
+ this.minute = 0;
450
+ if (this.timePicker || this.manualTimePicker) {
451
+ this.hour = this.selectedDate.getHours();
452
+ if (this.manualTimePicker)
453
+ this.minute = this.selectedDate.getMinutes();
454
+ else {
455
+ // round the minutes a multiple of 5
456
+ this.minute = Math.ceil(this.selectedDate.getMinutes() / 5) * 5;
457
+ this.selectedDate.setMinutes(this.minute);
458
+ }
459
+ }
460
+ this.buildCalendarGrid(this.refDate);
461
+ this.hours = Array.from(Array(24).keys()).map(i => '0'.concat(i.toString()).slice(-2));
462
+ this.minutes = Array.from(Array(12).keys()).map(i => '0'.concat((i * 5).toString()).slice(-2));
463
+ // build the weekdays based on the current locale
464
+ this.weekDays = new Array();
465
+ const refDateForWeekDays = new Date(SUNDAY);
466
+ refDateForWeekDays.setDate(refDateForWeekDays.getDate() - this.getLocalisedDay(refDateForWeekDays));
467
+ for (let i = 0; i < 7; i++) {
468
+ this.weekDays.push(refDateForWeekDays.toLocaleDateString(this._translate.getCurrentLang(), { weekday: 'short' }));
469
+ refDateForWeekDays.setDate(refDateForWeekDays.getDate() + 1);
470
+ }
471
+ if (this.min)
472
+ this.dateMin = new Date(this.min);
473
+ if (this.max)
474
+ this.dateMax = new Date(this.max);
475
+ }
476
+ getLocalisedDay(date) {
477
+ let n = date.getDay();
478
+ switch (this._translate.getCurrentLang()) {
479
+ case 'it':
480
+ n = --n < 0 ? 6 : n;
481
+ break;
482
+ }
483
+ return n;
484
+ }
485
+ buildCalendarGrid(refDate) {
486
+ // find the first day in the month: the important data here is the day of the week
487
+ const firstDateOfMonth = new Date(refDate.getFullYear(), refDate.getMonth(), 1);
488
+ // the following flag is used to divide the logic so I can fill the calendar
489
+ // also with the dates from the previous month, until there's space in the grid
490
+ let haventFoundFirstDay = true;
491
+ // index used to build the dates of the month, starting from the first one
492
+ let index = 1;
493
+ this.calendarGrid = new Array();
494
+ for (let i = 0; i < 6; i++) {
495
+ this.calendarGrid[i] = new Array();
496
+ for (let j = 0; j < 7; j++) {
497
+ if (haventFoundFirstDay) {
498
+ if (this.getLocalisedDay(firstDateOfMonth) === j) {
499
+ // note: considers Sunday
500
+ haventFoundFirstDay = false;
501
+ this.calendarGrid[i][j] = new Date(firstDateOfMonth);
502
+ // now the I've found the first date of the month I can fill the calendar
503
+ // the dates fromt the previous month, until there's space in the grid
504
+ for (let y = this.getLocalisedDay(firstDateOfMonth); y >= 0; y--) {
505
+ const d = new Date(firstDateOfMonth);
506
+ d.setDate(firstDateOfMonth.getDate() - y);
507
+ this.calendarGrid[i][this.getLocalisedDay(firstDateOfMonth) - y] = d;
508
+ }
509
+ }
510
+ }
511
+ else {
512
+ // fill the following dates until there's space in the grid
513
+ const d = new Date(firstDateOfMonth);
514
+ d.setDate(firstDateOfMonth.getDate() + index++);
515
+ this.calendarGrid[i][j] = d;
516
+ }
517
+ }
518
+ }
519
+ }
520
+ addYears(offset) {
521
+ this.refDate.setFullYear(this.refDate.getFullYear() + offset);
522
+ this.buildCalendarGrid(this.refDate);
523
+ }
524
+ setYear(year) {
525
+ if (!year)
526
+ return;
527
+ this.refDate.setFullYear(Number(year));
528
+ this.buildCalendarGrid(this.refDate);
529
+ }
530
+ addMonths(offset) {
531
+ this.refDate.setMonth(this.refDate.getMonth() + offset);
532
+ this.buildCalendarGrid(this.refDate);
533
+ }
534
+ async showMonths() {
535
+ const buttons = [];
536
+ const inputs = [];
537
+ const month = new Date(0);
538
+ for (let i = 1; i <= 12; i++) {
539
+ inputs.push({
540
+ type: 'radio',
541
+ label: month.toLocaleDateString(this._translate.getCurrentLang(), { month: 'long' }),
542
+ value: i.toString(),
543
+ checked: i === this.refDate.getMonth() + 1
544
+ });
545
+ month.setMonth(month.getMonth() + 1);
546
+ }
547
+ buttons.push({ text: this._translate._('COMMON.CANCEL'), role: 'cancel' });
548
+ buttons.push({
549
+ text: this._translate._('COMMON.SELECT'),
550
+ handler: (m) => {
551
+ this.refDate.setMonth(parseInt(m, 10) - 1);
552
+ this.buildCalendarGrid(this.refDate);
553
+ }
554
+ });
555
+ const alert = await this._alert.create({
556
+ header: this._translate._('IDEA_UNCOMMON.CALENDAR.MONTH'),
557
+ buttons,
558
+ inputs
559
+ });
560
+ alert.present();
561
+ }
562
+ isDateSelectable(date) {
563
+ if (this.dateMin && date < this.dateMin)
564
+ return false;
565
+ if (this.dateMax && date > this.dateMax)
566
+ return false;
567
+ return true;
568
+ }
569
+ selectDate(date) {
570
+ this.selectedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), this.hour, this.minute);
571
+ }
572
+ selectHour(hour) {
573
+ this.hour = Number(hour);
574
+ this.selectedDate.setHours(this.hour);
575
+ }
576
+ selectMinute(minute) {
577
+ this.minute = Number(minute);
578
+ this.selectedDate.setMinutes(this.minute);
579
+ }
580
+ isSameDay(a, b) {
581
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
582
+ }
583
+ isSameMonth(a, b) {
584
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
585
+ }
586
+ isSameHour(hour) {
587
+ return this.selectedDate.getHours() === Number(hour);
588
+ }
589
+ isSameMinute(minute) {
590
+ return this.selectedDate.getMinutes() === Number(minute);
591
+ }
592
+ save(reset) {
593
+ this._modal.dismiss(reset ? '' : this.selectedDate);
594
+ }
595
+ close() {
596
+ this._modal.dismiss();
597
+ }
598
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACalendarPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
599
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEACalendarPickerComponent, selector: "idea-calendar-picker", inputs: { inputDate: "inputDate", timePicker: "timePicker", manualTimePicker: "manualTimePicker", title: "title", hideClearButton: "hideClearButton", min: "min", max: "max" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-title>{{ title }}</ion-title>\n <ion-buttons slot=\"end\">\n @if (!hideClearButton) {\n <ion-button [title]=\"'IDEA_UNCOMMON.CALENDAR.RESET' | translate\" (click)=\"save(true)\">\n <ion-icon slot=\"icon-only\" name=\"arrow-undo\" />\n </ion-button>\n }\n <ion-button [title]=\"'IDEA_UNCOMMON.CALENDAR.CLOSE_WITHOUT_SAVING' | translate\" (click)=\"close()\">\n <ion-icon slot=\"icon-only\" name=\"close\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-grid fixed class=\"ion-no-padding\">\n <ion-row>\n <h1 class=\"headerLabel\">{{ 'IDEA_UNCOMMON.CALENDAR.SET_DATE' | translate }}</h1>\n </ion-row>\n <ion-row style=\"margin-bottom: 8px\">\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"5\">\n <ion-row class=\"ion-align-items-center ion-justify-content-center\">\n <ion-col class=\"ion-text-right ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addYears(-1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-back\" />\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding tappable\" [size]=\"6\">\n <ion-input\n type=\"text\"\n [placeholder]=\"'IDEA_UNCOMMON.CALENDAR.YEAR' | translate\"\n [value]=\"refDate.getFullYear()\"\n (ionFocus)=\"$event.target.value = ''\"\n (ionChange)=\"setYear($event?.target?.value)\"\n >\n </ion-input>\n </ion-col>\n <ion-col class=\"ion-text-left ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addYears(+1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-forward\" />\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"7\">\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addMonths(-1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-back\" />\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding tappable\" [size]=\"6\" (click)=\"showMonths()\">\n {{ refDate | dateLocale: 'MMMM' }}\n </ion-col>\n <ion-col class=\"ion-text-left ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addMonths(+1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-forward\" />\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-col>\n </ion-row>\n <ion-row style=\"margin-bottom: 8px\" class=\"headerLabel\">\n @for (ddd of weekDays; track ddd) {\n <ion-col class=\"ion-text-center ion-no-padding\">\n {{ ddd }}\n </ion-col>\n }\n </ion-row>\n @for (row of calendarGrid; track row; let i = $index) {\n <ion-row class=\"ion-align-items-center\">\n @for (col of row; track col; let j = $index) {\n <ion-col class=\"ion-text-center ion-no-padding\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"\n isSameDay(calendarGrid[i][j], today) || isSameDay(calendarGrid[i][j], selectedDate)\n ? 'primary'\n : isSameMonth(calendarGrid[i][j], refDate)\n ? 'dark'\n : 'medium'\n \"\n [fill]=\"\n isSameDay(calendarGrid[i][j], selectedDate)\n ? 'solid'\n : isSameDay(calendarGrid[i][j], today)\n ? 'outline'\n : 'clear'\n \"\n [disabled]=\"!isDateSelectable(calendarGrid[i][j])\"\n (click)=\"selectDate(calendarGrid[i][j])\"\n >\n {{ calendarGrid[i][j] | date: 'd' }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n }\n @if (timePicker || manualTimePicker) {\n <ion-row>\n <h1 class=\"headerLabel\">\n {{ 'IDEA_UNCOMMON.CALENDAR.SET_TIME' | translate }}\n </h1>\n </ion-row>\n }\n @if (timePicker) {\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-no-padding\" [size]=\"7\">\n <ion-row>\n @for (hour of hours; track hour) {\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"2\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"isSameHour(hour) ? 'primary' : 'dark'\"\n [fill]=\"isSameHour(hour) ? 'solid' : 'clear'\"\n (click)=\"selectHour(hour)\"\n >\n {{ hour }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"1\"> : </ion-col>\n <ion-col class=\"ion-no-padding\" [size]=\"4\">\n <ion-row>\n @for (minute of minutes; track minute) {\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"4\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"isSameMinute(minute) ? 'primary' : 'dark'\"\n [fill]=\"isSameMinute(minute) ? 'solid' : 'clear'\"\n (click)=\"selectMinute(minute)\"\n >\n {{ minute }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n </ion-col>\n </ion-row>\n }\n @if (manualTimePicker) {\n <ion-row class=\"ion-align-items-center manualTimePicker\">\n <ion-col class=\"ion-text-center\" [size]=\"12\">\n <input type=\"number\" min=\"0\" max=\"23\" [value]=\"hour\" (change)=\"selectHour($event.target.value)\" />\n <span>:</span>\n <input type=\"number\" min=\"0\" max=\"59\" [value]=\"minute\" (change)=\"selectMinute($event.target.value)\" />\n </ion-col>\n </ion-row>\n }\n </ion-grid>\n</ion-content>\n<ion-footer>\n <ion-toolbar color=\"ideaToolbar\" class=\"tappable\" (click)=\"save()\">\n <ion-title class=\"ion-text-center\">\n <ion-icon name=\"checkmark\" />\n </ion-title>\n </ion-toolbar>\n</ion-footer>\n", styles: ["ion-content{--background: var(--ion-color-white)}h1{margin:10px}.headerLabel{font-size:.7em;font-weight:700;text-transform:uppercase}ion-col,ion-button{font-size:.95em}ion-button{margin:3px 0}ion-footer ion-toolbar{--min-height: 40px}.tappable{cursor:pointer}.manualTimePicker{padding:20px}.manualTimePicker input[type=number],.manualTimePicker span{font-size:2.5em}.manualTimePicker input[type=number]{border:none;border-bottom:2px solid var(--ion-color-dark);text-align:center}.manualTimePicker span{padding-left:4px;padding-right:4px}\n"], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i1$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i1$1.IonRow, selector: "ion-row" }, { kind: "component", type: i1$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i1$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TextValueAccessor, selector: "ion-input:not([type=number]),ion-textarea,ion-searchbar" }, { kind: "pipe", type: i2.DatePipe, name: "date" }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }, { kind: "pipe", type: i3.IDEALocalizedDatePipe, name: "dateLocale" }] }); }
600
+ }
601
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACalendarPickerComponent, decorators: [{
602
+ type: Component,
603
+ args: [{ selector: 'idea-calendar-picker', template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-title>{{ title }}</ion-title>\n <ion-buttons slot=\"end\">\n @if (!hideClearButton) {\n <ion-button [title]=\"'IDEA_UNCOMMON.CALENDAR.RESET' | translate\" (click)=\"save(true)\">\n <ion-icon slot=\"icon-only\" name=\"arrow-undo\" />\n </ion-button>\n }\n <ion-button [title]=\"'IDEA_UNCOMMON.CALENDAR.CLOSE_WITHOUT_SAVING' | translate\" (click)=\"close()\">\n <ion-icon slot=\"icon-only\" name=\"close\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-grid fixed class=\"ion-no-padding\">\n <ion-row>\n <h1 class=\"headerLabel\">{{ 'IDEA_UNCOMMON.CALENDAR.SET_DATE' | translate }}</h1>\n </ion-row>\n <ion-row style=\"margin-bottom: 8px\">\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"5\">\n <ion-row class=\"ion-align-items-center ion-justify-content-center\">\n <ion-col class=\"ion-text-right ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addYears(-1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-back\" />\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding tappable\" [size]=\"6\">\n <ion-input\n type=\"text\"\n [placeholder]=\"'IDEA_UNCOMMON.CALENDAR.YEAR' | translate\"\n [value]=\"refDate.getFullYear()\"\n (ionFocus)=\"$event.target.value = ''\"\n (ionChange)=\"setYear($event?.target?.value)\"\n >\n </ion-input>\n </ion-col>\n <ion-col class=\"ion-text-left ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addYears(+1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-forward\" />\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"7\">\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addMonths(-1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-back\" />\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding tappable\" [size]=\"6\" (click)=\"showMonths()\">\n {{ refDate | dateLocale: 'MMMM' }}\n </ion-col>\n <ion-col class=\"ion-text-left ion-no-padding\" [size]=\"3\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"addMonths(+1)\">\n <ion-icon slot=\"icon-only\" name=\"chevron-forward\" />\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-col>\n </ion-row>\n <ion-row style=\"margin-bottom: 8px\" class=\"headerLabel\">\n @for (ddd of weekDays; track ddd) {\n <ion-col class=\"ion-text-center ion-no-padding\">\n {{ ddd }}\n </ion-col>\n }\n </ion-row>\n @for (row of calendarGrid; track row; let i = $index) {\n <ion-row class=\"ion-align-items-center\">\n @for (col of row; track col; let j = $index) {\n <ion-col class=\"ion-text-center ion-no-padding\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"\n isSameDay(calendarGrid[i][j], today) || isSameDay(calendarGrid[i][j], selectedDate)\n ? 'primary'\n : isSameMonth(calendarGrid[i][j], refDate)\n ? 'dark'\n : 'medium'\n \"\n [fill]=\"\n isSameDay(calendarGrid[i][j], selectedDate)\n ? 'solid'\n : isSameDay(calendarGrid[i][j], today)\n ? 'outline'\n : 'clear'\n \"\n [disabled]=\"!isDateSelectable(calendarGrid[i][j])\"\n (click)=\"selectDate(calendarGrid[i][j])\"\n >\n {{ calendarGrid[i][j] | date: 'd' }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n }\n @if (timePicker || manualTimePicker) {\n <ion-row>\n <h1 class=\"headerLabel\">\n {{ 'IDEA_UNCOMMON.CALENDAR.SET_TIME' | translate }}\n </h1>\n </ion-row>\n }\n @if (timePicker) {\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-no-padding\" [size]=\"7\">\n <ion-row>\n @for (hour of hours; track hour) {\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"2\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"isSameHour(hour) ? 'primary' : 'dark'\"\n [fill]=\"isSameHour(hour) ? 'solid' : 'clear'\"\n (click)=\"selectHour(hour)\"\n >\n {{ hour }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n </ion-col>\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"1\"> : </ion-col>\n <ion-col class=\"ion-no-padding\" [size]=\"4\">\n <ion-row>\n @for (minute of minutes; track minute) {\n <ion-col class=\"ion-text-center ion-no-padding\" [size]=\"4\">\n <ion-button\n size=\"small\"\n shape=\"round\"\n [color]=\"isSameMinute(minute) ? 'primary' : 'dark'\"\n [fill]=\"isSameMinute(minute) ? 'solid' : 'clear'\"\n (click)=\"selectMinute(minute)\"\n >\n {{ minute }}\n </ion-button>\n </ion-col>\n }\n </ion-row>\n </ion-col>\n </ion-row>\n }\n @if (manualTimePicker) {\n <ion-row class=\"ion-align-items-center manualTimePicker\">\n <ion-col class=\"ion-text-center\" [size]=\"12\">\n <input type=\"number\" min=\"0\" max=\"23\" [value]=\"hour\" (change)=\"selectHour($event.target.value)\" />\n <span>:</span>\n <input type=\"number\" min=\"0\" max=\"59\" [value]=\"minute\" (change)=\"selectMinute($event.target.value)\" />\n </ion-col>\n </ion-row>\n }\n </ion-grid>\n</ion-content>\n<ion-footer>\n <ion-toolbar color=\"ideaToolbar\" class=\"tappable\" (click)=\"save()\">\n <ion-title class=\"ion-text-center\">\n <ion-icon name=\"checkmark\" />\n </ion-title>\n </ion-toolbar>\n</ion-footer>\n", styles: ["ion-content{--background: var(--ion-color-white)}h1{margin:10px}.headerLabel{font-size:.7em;font-weight:700;text-transform:uppercase}ion-col,ion-button{font-size:.95em}ion-button{margin:3px 0}ion-footer ion-toolbar{--min-height: 40px}.tappable{cursor:pointer}.manualTimePicker{padding:20px}.manualTimePicker input[type=number],.manualTimePicker span{font-size:2.5em}.manualTimePicker input[type=number]{border:none;border-bottom:2px solid var(--ion-color-dark);text-align:center}.manualTimePicker span{padding-left:4px;padding-right:4px}\n"] }]
604
+ }], propDecorators: { inputDate: [{
605
+ type: Input
606
+ }], timePicker: [{
607
+ type: Input
608
+ }], manualTimePicker: [{
609
+ type: Input
610
+ }], title: [{
611
+ type: Input
612
+ }], hideClearButton: [{
613
+ type: Input
614
+ }], min: [{
615
+ type: Input
616
+ }], max: [{
617
+ type: Input
618
+ }] } });
619
+
620
+ class IDEADateTimeComponent {
621
+ constructor() {
622
+ this._modal = inject(ModalController);
623
+ this._translate = inject(IDEATranslationsService);
624
+ /**
625
+ * Whether to show the time picker (datetime) or not (date).
626
+ */
627
+ this.timePicker = false;
628
+ /**
629
+ * Whether to show the MANUAL time picker (datetime) or not (date).
630
+ */
631
+ this.manualTimePicker = false;
632
+ /**
633
+ * Whether to use the `epochISOString` format instead of `epochDateTime`.
634
+ */
635
+ this.useISOFormat = false;
636
+ /**
637
+ * If true, the component is disabled.
638
+ */
639
+ this.disabled = false;
640
+ /**
641
+ * If true, the obligatory dot is shown.
642
+ */
643
+ this.obligatory = false;
644
+ /**
645
+ * If true, hidew the clear button in the header.
646
+ */
647
+ this.hideClearButton = false;
648
+ this.dateChange = new EventEmitter();
649
+ this.iconSelect = new EventEmitter();
650
+ this.isOpening = false;
651
+ }
652
+ ngOnInit() {
653
+ this.langChangeSubscription = this._translate.onLangChange.subscribe(() => {
654
+ this.valueToDisplay = this.getValueToDisplay(this.date);
655
+ });
656
+ }
657
+ ngOnDestroy() {
658
+ if (this.langChangeSubscription)
659
+ this.langChangeSubscription.unsubscribe();
660
+ }
661
+ ngOnChanges(changes) {
662
+ if (changes.date || changes.timePicker)
663
+ this.valueToDisplay = this.getValueToDisplay(this.date);
664
+ }
665
+ async openCalendarPicker() {
666
+ if (this.disabled || this.isOpening)
667
+ return;
668
+ this.isOpening = true;
669
+ const modal = await this._modal.create({
670
+ component: IDEACalendarPickerComponent,
671
+ componentProps: {
672
+ inputDate: this.date,
673
+ title: this.label,
674
+ timePicker: this.timePicker,
675
+ manualTimePicker: this.manualTimePicker,
676
+ hideClearButton: this.hideClearButton,
677
+ min: this.min,
678
+ max: this.max
679
+ }
680
+ });
681
+ modal.onDidDismiss().then(({ data }) => {
682
+ if (data !== undefined && data !== null) {
683
+ if (data === '')
684
+ this.doSelect(null);
685
+ else
686
+ this.doSelect(data);
687
+ }
688
+ });
689
+ modal.present();
690
+ this.isOpening = false;
691
+ }
692
+ getValueToDisplay(date) {
693
+ return !date
694
+ ? ''
695
+ : this._translate.formatDate(date, this.timePicker || this.manualTimePicker ? 'd MMM yyyy, HH:mm' : 'mediumDate');
696
+ }
697
+ doSelect(date) {
698
+ this.dateChange.emit(date ? (this.useISOFormat ? date.toISOString() : date.valueOf()) : null);
699
+ }
700
+ doIconSelect(event) {
701
+ if (event)
702
+ event.stopPropagation();
703
+ this.iconSelect.emit();
704
+ }
705
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
706
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEADateTimeComponent, selector: "idea-date-time", inputs: { date: "date", timePicker: "timePicker", manualTimePicker: "manualTimePicker", useISOFormat: "useISOFormat", label: "label", icon: "icon", iconColor: "iconColor", lines: "lines", color: "color", placeholder: "placeholder", disabled: "disabled", obligatory: "obligatory", hideClearButton: "hideClearButton", min: "min", max: "max" }, outputs: { dateChange: "dateChange", iconSelect: "iconSelect" }, usesOnChanges: true, ngImport: i0, template: "<ion-item\n class=\"dateTimeItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [title]=\"placeholder || label || ''\"\n [button]=\"!disabled\"\n [disabled]=\"isOpening\"\n [class.withLabel]=\"label\"\n (click)=\"openCalendarPicker()\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" [class.marginTop]=\"label\" (click)=\"doIconSelect($event)\">\n <ion-icon [icon]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label position=\"stacked\" [class.selectable]=\"!disabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"value\" [class.selectable]=\"!disabled\">\n @if (!valueToDisplay && !disabled) {\n <ion-text class=\"placeholder\" [class.selectable]=\"!disabled\">\n {{ placeholder }}\n </ion-text>\n }\n {{ valueToDisplay }}\n </ion-label>\n @if (!disabled) {\n <ion-icon slot=\"end\" icon=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".dateTimeItem{min-height:48px;height:auto}.dateTimeItem .description{margin:10px 0;height:20px;line-height:20px;width:100%}.dateTimeItem .placeholder{color:var(--ion-color-medium)}.dateTimeItem .value{white-space:nowrap}.dateTimeItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.dateTimeItem.withLabel{min-height:58px;height:auto}.dateTimeItem.withLabel .selectIcon{padding-top:25px}.dateTimeItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}\n"], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }] }); }
707
+ }
708
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeComponent, decorators: [{
709
+ type: Component,
710
+ args: [{ selector: 'idea-date-time', template: "<ion-item\n class=\"dateTimeItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [title]=\"placeholder || label || ''\"\n [button]=\"!disabled\"\n [disabled]=\"isOpening\"\n [class.withLabel]=\"label\"\n (click)=\"openCalendarPicker()\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" [class.marginTop]=\"label\" (click)=\"doIconSelect($event)\">\n <ion-icon [icon]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label position=\"stacked\" [class.selectable]=\"!disabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"value\" [class.selectable]=\"!disabled\">\n @if (!valueToDisplay && !disabled) {\n <ion-text class=\"placeholder\" [class.selectable]=\"!disabled\">\n {{ placeholder }}\n </ion-text>\n }\n {{ valueToDisplay }}\n </ion-label>\n @if (!disabled) {\n <ion-icon slot=\"end\" icon=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".dateTimeItem{min-height:48px;height:auto}.dateTimeItem .description{margin:10px 0;height:20px;line-height:20px;width:100%}.dateTimeItem .placeholder{color:var(--ion-color-medium)}.dateTimeItem .value{white-space:nowrap}.dateTimeItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.dateTimeItem.withLabel{min-height:58px;height:auto}.dateTimeItem.withLabel .selectIcon{padding-top:25px}.dateTimeItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}\n"] }]
711
+ }], propDecorators: { date: [{
712
+ type: Input
713
+ }], timePicker: [{
714
+ type: Input
715
+ }], manualTimePicker: [{
716
+ type: Input
717
+ }], useISOFormat: [{
718
+ type: Input
719
+ }], label: [{
720
+ type: Input
721
+ }], icon: [{
722
+ type: Input
723
+ }], iconColor: [{
724
+ type: Input
725
+ }], lines: [{
726
+ type: Input
727
+ }], color: [{
728
+ type: Input
729
+ }], placeholder: [{
730
+ type: Input
731
+ }], disabled: [{
732
+ type: Input
733
+ }], obligatory: [{
734
+ type: Input
735
+ }], hideClearButton: [{
736
+ type: Input
737
+ }], min: [{
738
+ type: Input
739
+ }], max: [{
740
+ type: Input
741
+ }], dateChange: [{
742
+ type: Output
743
+ }], iconSelect: [{
744
+ type: Output
745
+ }] } });
746
+
747
+ class IDEADateTimeModule {
748
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
749
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeModule, declarations: [IDEADateTimeComponent, IDEACalendarPickerComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEADateTimeComponent, IDEACalendarPickerComponent] }); }
750
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
751
+ }
752
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADateTimeModule, decorators: [{
753
+ type: NgModule,
754
+ args: [{
755
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
756
+ declarations: [IDEADateTimeComponent, IDEACalendarPickerComponent],
757
+ exports: [IDEADateTimeComponent, IDEACalendarPickerComponent]
758
+ }]
759
+ }] });
760
+
761
+ class IDEADurationComponent {
762
+ constructor() {
763
+ /**
764
+ * On change event. It emits a number of seconds representing the duration.
765
+ */
766
+ this.set = new EventEmitter();
767
+ this.default = this.hours = this.minutes = this.seconds = 0;
768
+ }
769
+ ngOnChanges(changes) {
770
+ // assign a default value to the duration
771
+ if (changes['default'] && changes['default'].isFirstChange()) {
772
+ const refDate = new Date(0);
773
+ refDate.setHours(0, 0, 0);
774
+ refDate.setSeconds(refDate.getSeconds() + this.default);
775
+ this.hours = refDate.getHours();
776
+ this.minutes = refDate.getMinutes();
777
+ this.seconds = this.hideSeconds ? 0 : refDate.getSeconds();
778
+ }
779
+ }
780
+ /**
781
+ * When one of the parts changes, emit the new duration.
782
+ */
783
+ setDuration(type) {
784
+ switch (type) {
785
+ case 'hours':
786
+ const hh = Number(this.hours) || 0;
787
+ this.hours = hh > 23 ? 23 : hh < 0 ? 0 : hh;
788
+ break;
789
+ case 'minutes':
790
+ const mm = Number(this.minutes) || 0;
791
+ this.minutes = mm > 59 ? 59 : mm < 0 ? 0 : mm;
792
+ break;
793
+ case 'seconds':
794
+ const ss = Number(this.seconds) || 0;
795
+ if (this.hideSeconds)
796
+ this.seconds = 0;
797
+ else
798
+ this.seconds = ss > 59 ? 59 : ss < 0 ? 0 : ss;
799
+ break;
800
+ }
801
+ this.set.emit(this.hours * 3600 + this.minutes * 60 + this.seconds);
802
+ }
803
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
804
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEADurationComponent, selector: "idea-duration", inputs: { default: "default", label: "label", icon: "icon", title: "title", disabled: "disabled", obligatory: "obligatory", lines: "lines", color: "color", hideSeconds: "hideSeconds", shortLabels: "shortLabels" }, outputs: { set: "set" }, usesOnChanges: true, ngImport: i0, template: "<ion-item class=\"durationItem\" [lines]=\"lines\" [color]=\"color\" [title]=\"title || label\">\n @if (icon) {\n <ion-icon slot=\"start\" [name]=\"icon\" />\n }\n @if (!icon && label) {\n <ion-label position=\"stacked\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"value\">\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"23\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"hours\"\n (ionChange)=\"setDuration('hours')\"\n />\n <ion-text>{{\n ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()\n }}</ion-text>\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"59\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"minutes\"\n (ionChange)=\"setDuration('minutes')\"\n />\n <ion-text>{{\n ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()\n }}</ion-text>\n @if (!hideSeconds) {\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"59\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"seconds\"\n (ionChange)=\"setDuration('seconds')\"\n />\n }\n @if (!hideSeconds) {\n <ion-text>\n {{ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase() }}\n </ion-text>\n }\n </ion-label>\n</ion-item>\n", styles: [".durationItem .value{max-width:none;width:100%;pointer-events:all}.durationItem .value ion-input{display:inline-block;max-width:35px;text-align:right}.durationItem .value ion-text{margin-left:10px;margin-right:20px;font-size:.8em}\n"], dependencies: [{ kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.NumericValueAccessor, selector: "ion-input[type=number],ion-range" }, { kind: "directive", type: i1$1.IonMinValidator, selector: "ion-input[type=number][min][formControlName],ion-input[type=number][min][formControl],ion-input[type=number][min][ngModel]" }, { kind: "directive", type: i1$1.IonMaxValidator, selector: "ion-input[type=number][max][formControlName],ion-input[type=number][max][formControl],ion-input[type=number][max][ngModel]" }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
805
+ }
806
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationComponent, decorators: [{
807
+ type: Component,
808
+ args: [{ selector: 'idea-duration', template: "<ion-item class=\"durationItem\" [lines]=\"lines\" [color]=\"color\" [title]=\"title || label\">\n @if (icon) {\n <ion-icon slot=\"start\" [name]=\"icon\" />\n }\n @if (!icon && label) {\n <ion-label position=\"stacked\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"value\">\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"23\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"hours\"\n (ionChange)=\"setDuration('hours')\"\n />\n <ion-text>{{\n ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()\n }}</ion-text>\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"59\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"minutes\"\n (ionChange)=\"setDuration('minutes')\"\n />\n <ion-text>{{\n ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()\n }}</ion-text>\n @if (!hideSeconds) {\n <ion-input\n type=\"number\"\n min=\"0\"\n max=\"59\"\n inputmode=\"numeric\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"seconds\"\n (ionChange)=\"setDuration('seconds')\"\n />\n }\n @if (!hideSeconds) {\n <ion-text>\n {{ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase() }}\n </ion-text>\n }\n </ion-label>\n</ion-item>\n", styles: [".durationItem .value{max-width:none;width:100%;pointer-events:all}.durationItem .value ion-input{display:inline-block;max-width:35px;text-align:right}.durationItem .value ion-text{margin-left:10px;margin-right:20px;font-size:.8em}\n"] }]
809
+ }], ctorParameters: () => [], propDecorators: { default: [{
810
+ type: Input
811
+ }], label: [{
812
+ type: Input
813
+ }], icon: [{
814
+ type: Input
815
+ }], title: [{
816
+ type: Input
817
+ }], disabled: [{
818
+ type: Input
819
+ }], obligatory: [{
820
+ type: Input
821
+ }], lines: [{
822
+ type: Input
823
+ }], color: [{
824
+ type: Input
825
+ }], hideSeconds: [{
826
+ type: Input
827
+ }], shortLabels: [{
828
+ type: Input
829
+ }], set: [{
830
+ type: Output
831
+ }] } });
832
+
833
+ class IDEADurationModule {
834
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
835
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationModule, declarations: [IDEADurationComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEADurationComponent] }); }
836
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
837
+ }
838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEADurationModule, decorators: [{
839
+ type: NgModule,
840
+ args: [{
841
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
842
+ declarations: [IDEADurationComponent],
843
+ exports: [IDEADurationComponent]
844
+ }]
845
+ }] });
846
+
847
+ class IDEAErrorReportingService {
848
+ constructor() {
849
+ this._env = inject(IDEAEnvironment);
850
+ this._http = inject(HttpClient);
851
+ this._platform = inject(Platform);
852
+ this.apiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
853
+ this.apiUrl = 'https://'.concat([this._env.idea.ideaApi?.url, this.apiStage].filter(x => x).join('/'));
854
+ }
855
+ /**
856
+ * Send the error report to the back-end.
857
+ */
858
+ sendReport(error, forceSend) {
859
+ return new Promise(resolve => {
860
+ // skip in case we don't want to send the report or the report is useless (blank error)
861
+ if ((!this.shouldSend() && !forceSend) || !error?.name)
862
+ return resolve();
863
+ // prepare and send the report
864
+ const report = new ErrorReport({
865
+ version: this._env.idea.app.version,
866
+ stage: this.apiStage,
867
+ client: this.getClientInfo(),
868
+ type: error.name,
869
+ error: error.message,
870
+ stack: error.stack
871
+ });
872
+ this._http
873
+ .post(this.apiUrl.concat(`/projects/${this._env.idea.project}/errorReporting`), report)
874
+ .toPromise()
875
+ .catch(() => { }) // note: never throw an error when reporting an error
876
+ .finally(() => resolve());
877
+ });
878
+ }
879
+ /**
880
+ * Whether we should send the reporting or we are in a scenario in which we should skip it.
881
+ */
882
+ shouldSend() {
883
+ return this.apiStage === 'prod';
884
+ }
885
+ /**
886
+ * Acquire the client's info.
887
+ */
888
+ getClientInfo() {
889
+ return new ClientInfo({
890
+ timestamp: new Date().toISOString(),
891
+ platform: this._platform.platforms().join(' '),
892
+ screenWidth: this._platform.width(),
893
+ screenHeight: this._platform.height(),
894
+ isLandScape: this._platform.isLandscape(),
895
+ url: this._platform.url(),
896
+ referrer: document.referrer,
897
+ isOnline: navigator.onLine,
898
+ language: navigator.language,
899
+ userAgent: navigator.userAgent
900
+ });
901
+ }
902
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAErrorReportingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
903
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAErrorReportingService }); }
904
+ }
905
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAErrorReportingService, decorators: [{
906
+ type: Injectable
907
+ }], ctorParameters: () => [] });
908
+
909
+ /**
910
+ * Utility to monitor the network connection.
911
+ * Note: don't use it directly, if you're also using IDEAOfflineDataService.
912
+ */
913
+ class IDEAOfflineService {
914
+ constructor() {
915
+ // get an instant status
916
+ this._isOffline = !navigator.onLine;
917
+ if (Network) {
918
+ // set the basic listener for changes
919
+ this.listenerHandler = Network.addListener('networkStatusChange', status => (this._isOffline = !status.connected));
920
+ // create the observable to subscribe to the network changes; note: it won't run until subscribed
921
+ this.observable = new Observable(observer => {
922
+ // remove the basic listener add add one that supports a subscription
923
+ Network.removeAllListeners();
924
+ this.listenerHandler = Network.addListener('networkStatusChange', status => {
925
+ this._isOffline = !status.connected;
926
+ if (observer)
927
+ observer.next(status.connected);
928
+ });
929
+ });
930
+ }
931
+ }
932
+ /**
933
+ * Subscribe to be notified when the connection status changes.
934
+ */
935
+ subscribe(callback) {
936
+ return this.observable.subscribe(callback);
937
+ }
938
+ /**
939
+ * Quickly check the connection status.
940
+ */
941
+ isOnline() {
942
+ return !this._isOffline;
943
+ }
944
+ /**
945
+ * Quickly check the connection status.
946
+ */
947
+ isOffline() {
948
+ return this._isOffline;
949
+ }
950
+ /**
951
+ * Quickly check for online connection.
952
+ */
953
+ async check() {
954
+ if (!Network)
955
+ return false;
956
+ const status = await Network.getStatus();
957
+ this._isOffline = !status.connected;
958
+ return status.connected;
959
+ }
960
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
961
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineService }); }
962
+ }
963
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineService, decorators: [{
964
+ type: Injectable
965
+ }], ctorParameters: () => [] });
966
+
967
+ /**
968
+ * To communicate with an AWS's API Gateway istance.
969
+ *
970
+ * Includes a rundimental cache on GET requests, until Android doesn't support SWs on local files.
971
+ *
972
+ * Note: requires an `AWSAPIAuthToken` variable to be set by IDEATinCanService service.
973
+ */
974
+ class IDEAAWSAPIService {
975
+ constructor() {
976
+ this._env = inject(IDEAEnvironment);
977
+ this._http = inject(HttpClient);
978
+ this._platform = inject(Platform);
979
+ this._tc = inject(IDEATinCanService);
980
+ this._storage = inject(IDEAStorageService);
981
+ this._errorReporting = inject(IDEAErrorReportingService);
982
+ this._offline = inject(IDEAOfflineService);
983
+ this.apiStage = this._env.idea.api?.stage || this._env.idea.api?.version;
984
+ this.apiUrlProject = 'https://'.concat([this._env.idea.api?.url, this.apiStage].filter(x => x).join('/'));
985
+ this.ideaApiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
986
+ this.apiUrlIDEA = 'https://'.concat([this._env.idea.ideaApi?.url, this.ideaApiStage].filter(x => x).join('/'));
987
+ }
988
+ /**
989
+ * Execute an online API request.
990
+ * @param resource resource name (e.g. `users`)
991
+ * @param method enum HTTP methods
992
+ * @param options the request options
993
+ */
994
+ request(resource, method, options) {
995
+ return new Promise((resolve, reject) => {
996
+ const opt = options || {};
997
+ // prepare the url
998
+ const url = this.prepareURL(resource, opt);
999
+ // prepare the headers and set the Authorization token
1000
+ const headers = this.prepareHeaders(opt.headers);
1001
+ // prepare the query params and add some info about the client
1002
+ const params = this.prepareQueryParams(opt.params, true);
1003
+ // execute the request
1004
+ let req;
1005
+ switch (method) {
1006
+ case 'HEAD':
1007
+ req = this._http.head(url, { headers, params });
1008
+ break;
1009
+ case 'POST':
1010
+ req = this._http.post(url, opt.body, { headers, params });
1011
+ break;
1012
+ case 'PUT':
1013
+ req = this._http.put(url, opt.body, { headers, params });
1014
+ break;
1015
+ case 'PATCH':
1016
+ req = this._http.patch(url, opt.body, { headers, params });
1017
+ break;
1018
+ case 'DELETE':
1019
+ req = this._http.delete(url, { headers, params });
1020
+ break;
1021
+ case 'GET':
1022
+ default:
1023
+ req = this._http.get(url, { headers, params });
1024
+ }
1025
+ // handle the request response
1026
+ req.subscribe((res) => resolve(res), async (err) => {
1027
+ // check if the request failed for network reasons (to trigger offline mode if needed)
1028
+ await this._offline.check();
1029
+ // (async) send a report, if wanted
1030
+ if (opt.reportError)
1031
+ this._errorReporting.sendReport(err);
1032
+ // fix and return the error
1033
+ this.fixErrMessageBeforeReject(err, reject);
1034
+ });
1035
+ });
1036
+ }
1037
+ /**
1038
+ * Prepare the URL to which execute the request.
1039
+ */
1040
+ prepareURL(resource, opt) {
1041
+ // decide whether to use IDEA's API or the project's API (or an alternative API)
1042
+ let url = opt.idea ? this.apiUrlIDEA : opt.alternativeAPI || this.apiUrlProject;
1043
+ // prepare a single-resource request (by id) or a normal one
1044
+ url = `${url}/${resource}`;
1045
+ if (opt.resourceId)
1046
+ url = `${url}/${encodeURIComponent(opt.resourceId)}`;
1047
+ return url;
1048
+ }
1049
+ /**
1050
+ * Prepare the headers object to send with a request.
1051
+ * Note: it always adds the Authorization token (`AWSAPIAuthToken`) and the API key (`AWSAPIKey`), if set.
1052
+ */
1053
+ prepareHeaders(h) {
1054
+ // preare the headers; note: HttpHeaders is immutable!
1055
+ let headers = new HttpHeaders(h || null);
1056
+ // set the Authorization token
1057
+ if (!headers.get('Authorization') && this._tc.get('AWSAPIAuthToken'))
1058
+ headers = headers.set('Authorization', this._tc.get('AWSAPIAuthToken'));
1059
+ // set the API key
1060
+ if (!headers.get('X-API-Key') && this._tc.get('AWSAPIKey'))
1061
+ headers = headers.set('X-API-Key', this._tc.get('AWSAPIKey'));
1062
+ return headers;
1063
+ }
1064
+ /**
1065
+ * Prepare the queryParams object to send with a request; if requested, add some info about the client.
1066
+ */
1067
+ prepareQueryParams(qp, addClientInfo) {
1068
+ // prepare the query params; note: HttpParams is immutable!
1069
+ let searchParams = new HttpParams();
1070
+ if (qp && qp instanceof HttpParams)
1071
+ searchParams = qp;
1072
+ else if (qp)
1073
+ for (const prop in qp)
1074
+ if (qp[prop])
1075
+ searchParams = searchParams.set(prop, qp[prop]);
1076
+ // if requested, add app version and client platform to the info we send to the back-end
1077
+ if (addClientInfo) {
1078
+ searchParams = searchParams.set('_v', this._env.idea.app.version);
1079
+ searchParams = searchParams.set('_p', this._platform.platforms().join(' '));
1080
+ }
1081
+ return searchParams;
1082
+ }
1083
+ /**
1084
+ * Converts the error message to be readable.
1085
+ */
1086
+ fixErrMessageBeforeReject(err, reject) {
1087
+ let e;
1088
+ try {
1089
+ if (!err || !err.error)
1090
+ e = {};
1091
+ else if (typeof err.error === 'string')
1092
+ e = JSON.parse(err.error);
1093
+ else
1094
+ e = err.error;
1095
+ }
1096
+ catch (_) {
1097
+ e = {};
1098
+ }
1099
+ reject(new Error(e.message || 'Unknown error!'));
1100
+ }
1101
+ /**
1102
+ * Use the HttpClient to execute a raw request.
1103
+ */
1104
+ rawRequest() {
1105
+ return this._http;
1106
+ }
1107
+ /**
1108
+ * HEAD request.
1109
+ * @param resource resource name (e.g. `users`)
1110
+ * @param options the request options
1111
+ */
1112
+ headResource(resource, options) {
1113
+ return this.request(resource, 'HEAD', options);
1114
+ }
1115
+ /**
1116
+ * GET request. Include options to cache the requests (`options.useCache`), to work also offline.
1117
+ * @param resource resource name (e.g. `users`)
1118
+ * @param options the request options
1119
+ */
1120
+ getResource(resource, options) {
1121
+ return new Promise((resolve, reject) => {
1122
+ const opt = options || {};
1123
+ // if offline and with a cache mode set, force the request to the cache
1124
+ if (!navigator.onLine && opt.useCache)
1125
+ opt.useCache = CacheModes.CACHE_ONLY;
1126
+ // execute the GET request online or through the cache, depending on the chosen mode
1127
+ switch (opt.useCache) {
1128
+ case CacheModes.CACHE_ONLY:
1129
+ // return the result from the cache, without retrying online if a result wasn't found
1130
+ this.getFromCache(resource, opt).then((res) => {
1131
+ if (res)
1132
+ resolve(res);
1133
+ else
1134
+ reject();
1135
+ });
1136
+ break;
1137
+ case CacheModes.CACHE_FIRST:
1138
+ // return the result from the cache
1139
+ this.getFromCache(resource, opt).then((localRes) => {
1140
+ // check whether the local resource is present (element or list)
1141
+ if (localRes && (opt.resourceId || localRes.length)) {
1142
+ resolve(localRes);
1143
+ // asynchrounously execute the request online, to update the cache with latest data
1144
+ this.request(resource, 'GET', opt)
1145
+ .then((cloudRes) => {
1146
+ // update the cache (if it fails, it's ok)
1147
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
1148
+ })
1149
+ .catch(() => { }); // we already returned the result, an error is acceptable
1150
+ }
1151
+ else {
1152
+ // if the result isn't found in the cache, return the result of the online request
1153
+ this.request(resource, 'GET', opt)
1154
+ .then((res) => {
1155
+ resolve(res);
1156
+ // update the cache (if it fails, it's ok)
1157
+ this.putInCache(resource, res, opt).catch(() => { });
1158
+ })
1159
+ .catch((err) => reject(err)); // element not found
1160
+ }
1161
+ });
1162
+ break;
1163
+ case CacheModes.NETWORK_FIRST:
1164
+ // return the result from an online request
1165
+ this.request(resource, 'GET', opt)
1166
+ .then((cloudRes) => {
1167
+ resolve(cloudRes);
1168
+ // asynchrounously get the same element from cache and decide whether to update or not
1169
+ this.getFromCache(resource, opt).then(() => {
1170
+ // update the cache (if it fails, it's ok)
1171
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
1172
+ });
1173
+ })
1174
+ .catch(() => {
1175
+ // if a request to the network fails, check in cache
1176
+ this.getFromCache(resource, opt).then((res) => {
1177
+ if (res)
1178
+ resolve(res);
1179
+ else
1180
+ reject();
1181
+ });
1182
+ });
1183
+ break;
1184
+ default:
1185
+ /* CacheModes.NO_CACHE */
1186
+ // return the result from an online request, with no cache involved
1187
+ this.request(resource, 'GET', opt)
1188
+ .then((res) => resolve(res))
1189
+ .catch((err) => reject(err));
1190
+ }
1191
+ });
1192
+ }
1193
+ /**
1194
+ * Observe a GET request. Include options to cache the requests (`options.useCache`), to work also offline.
1195
+ * @param resource resource name (e.g. `users`)
1196
+ * @param options the request options
1197
+ */
1198
+ getResourceObserver(resource, options) {
1199
+ return new Observable(observer => {
1200
+ const opt = options || {};
1201
+ // if offline and with a cache mode set, force the request to the cache
1202
+ if (!navigator.onLine && opt.useCache)
1203
+ opt.useCache = CacheModes.CACHE_ONLY;
1204
+ // execute the GET request online or through the cache, depending on the chosen mode
1205
+ switch (opt.useCache) {
1206
+ case CacheModes.CACHE_ONLY:
1207
+ // return the result from the cache, without retrying online if a result wasn't found
1208
+ this.getFromCache(resource, opt).then((res) => {
1209
+ if (res)
1210
+ observer.next(res);
1211
+ else
1212
+ observer.error();
1213
+ });
1214
+ break;
1215
+ case CacheModes.CACHE_FIRST:
1216
+ // return the result from the cache
1217
+ this.getFromCache(resource, opt).then((localRes) => {
1218
+ if (localRes) {
1219
+ observer.next(localRes);
1220
+ // asynchrounously execute the request online, to update the cache with latest data
1221
+ this.request(resource, 'GET', opt)
1222
+ .then((cloudRes) => {
1223
+ observer.next(cloudRes);
1224
+ // update the cache (if it fails, it's ok)
1225
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
1226
+ })
1227
+ .catch(() => { }); // we already returned the result, an error is acceptable
1228
+ }
1229
+ else {
1230
+ // if the result isn't found in the cache, return the result of the online request
1231
+ this.request(resource, 'GET', opt)
1232
+ .then((res) => {
1233
+ observer.next(res);
1234
+ // update the cache (if it fails, it's ok)
1235
+ this.putInCache(resource, res, opt).catch(() => { });
1236
+ })
1237
+ .catch((err) => observer.error(err)); // element not found
1238
+ }
1239
+ });
1240
+ break;
1241
+ case CacheModes.NETWORK_FIRST:
1242
+ // return the result from an online request
1243
+ this.request(resource, 'GET', opt)
1244
+ .then((cloudRes) => {
1245
+ observer.next(cloudRes);
1246
+ // update the cache (if it fails, it's ok)
1247
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
1248
+ })
1249
+ .catch(() => {
1250
+ // if a request to the network fails, check in cache
1251
+ this.getFromCache(resource, opt).then((res) => {
1252
+ if (res)
1253
+ observer.next(res);
1254
+ else
1255
+ observer.error();
1256
+ });
1257
+ });
1258
+ break;
1259
+ default:
1260
+ /* CacheModes.NO_CACHE */
1261
+ // return the result from an online request, with no cache involved
1262
+ this.request(resource, 'GET', opt)
1263
+ .then((res) => observer.next(res))
1264
+ .catch((err) => observer.error(err));
1265
+ }
1266
+ });
1267
+ }
1268
+ /**
1269
+ * Execute a GET request from the local storage.
1270
+ * @param resource resource name (e.g. `users`)
1271
+ * @param options the request options
1272
+ */
1273
+ getFromCache(resource, options) {
1274
+ return new Promise(resolve => {
1275
+ const opt = options || {};
1276
+ // prepare the url and the query params
1277
+ const url = this.prepareURL(resource, opt);
1278
+ const queryParams = this.prepareQueryParams(opt.params);
1279
+ // get from storage, by the complete url
1280
+ this._storage
1281
+ .get(url.concat(queryParams.toString()))
1282
+ .then((res) => (res ? resolve(res) : opt.resourceId ? resolve(null) : resolve([])));
1283
+ });
1284
+ }
1285
+ /**
1286
+ * Execute a PUT request in the local storage.
1287
+ * @param resource resource name (e.g. `users`)
1288
+ * @param data resource data
1289
+ * @param options the request options
1290
+ */
1291
+ putInCache(resource, data, options) {
1292
+ return new Promise((resolve, reject) => {
1293
+ const opt = options || {};
1294
+ // prepare the url and the query params
1295
+ const url = this.prepareURL(resource, opt);
1296
+ const queryParams = this.prepareQueryParams(opt.params);
1297
+ // put in the storage, by the complete url
1298
+ this._storage
1299
+ .set(url.concat(queryParams.toString()), data)
1300
+ .then(() => resolve())
1301
+ .catch((err) => reject(err));
1302
+ });
1303
+ }
1304
+ /**
1305
+ * Execute a DELETE request in the local storage.
1306
+ * @param resource resource name (e.g. `users`)
1307
+ * @param options the request options
1308
+ */
1309
+ deleteFromCache(resource, options) {
1310
+ return new Promise((resolve, reject) => {
1311
+ const opt = options || {};
1312
+ // prepare the url and the query params
1313
+ const url = this.prepareURL(resource, opt);
1314
+ const queryParams = this.prepareQueryParams(opt.params);
1315
+ // delete from storage, by the complete url
1316
+ this._storage
1317
+ .remove(url.concat(queryParams.toString()))
1318
+ .then(() => resolve())
1319
+ .catch((err) => reject(err));
1320
+ });
1321
+ }
1322
+ /**
1323
+ * POST request.
1324
+ * @param resource resource name (e.g. `users`)
1325
+ * @param options the request options
1326
+ */
1327
+ postResource(resource, options) {
1328
+ return this.request(resource, 'POST', options);
1329
+ }
1330
+ /**
1331
+ * PUT request.
1332
+ * @param resource resource name (e.g. `users`)
1333
+ * @param options the request options
1334
+ */
1335
+ putResource(resource, options) {
1336
+ return this.request(resource, 'PUT', options);
1337
+ }
1338
+ /**
1339
+ * PATCH request.
1340
+ * @param resource resource name (e.g. `users`)
1341
+ * @param options the request options
1342
+ */
1343
+ patchResource(resource, options) {
1344
+ return this.request(resource, 'PATCH', options);
1345
+ }
1346
+ /**
1347
+ * DELETE request.
1348
+ * @param resource resource name (e.g. `users`)
1349
+ * @param options the request options
1350
+ */
1351
+ deleteResource(resource, options) {
1352
+ return this.request(resource, 'DELETE', options);
1353
+ }
1354
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPIService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1355
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPIService }); }
1356
+ }
1357
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPIService, decorators: [{
1358
+ type: Injectable
1359
+ }], ctorParameters: () => [] });
1360
+ /**
1361
+ * The options of an API request.
1362
+ */
1363
+ class APIRequestOption {
1364
+ }
1365
+ /**
1366
+ * Set of usable cache modes.
1367
+ */
1368
+ var CacheModes;
1369
+ (function (CacheModes) {
1370
+ /**
1371
+ * Return the result from an online request, with no cache involved.
1372
+ */
1373
+ CacheModes[CacheModes["NO_CACHE"] = 0] = "NO_CACHE";
1374
+ /**
1375
+ * Return the result from an online request, storing the result in the cache, to update it with the latest data.
1376
+ */
1377
+ CacheModes[CacheModes["NETWORK_FIRST"] = 1] = "NETWORK_FIRST";
1378
+ /**
1379
+ * Return the result from the cache, but also execute the request online, to update the cache with the latest data.
1380
+ */
1381
+ CacheModes[CacheModes["CACHE_FIRST"] = 2] = "CACHE_FIRST";
1382
+ /**
1383
+ * Return the result from the cache, without retrying online if a result wasn't found.
1384
+ */
1385
+ CacheModes[CacheModes["CACHE_ONLY"] = 3] = "CACHE_ONLY";
1386
+ })(CacheModes || (CacheModes = {}));
1387
+
1388
+ class IDEAEchoPage {
1389
+ constructor() {
1390
+ this._env = inject(IDEAEnvironment);
1391
+ this._route = inject(ActivatedRoute);
1392
+ this._tc = inject(IDEATinCanService);
1393
+ this._loading = inject(IDEALoadingService);
1394
+ this._API = inject(IDEAAWSAPIService);
1395
+ this._translate = inject(IDEATranslationsService);
1396
+ }
1397
+ ngOnInit() {
1398
+ const request = this._route.snapshot.paramMap.get('request') || this._route.snapshot.queryParams.request;
1399
+ const code = this._route.snapshot.queryParams.code;
1400
+ const user = decodeURIComponent(this._route.snapshot.queryParams.user);
1401
+ const state = this._route.snapshot.queryParams.state;
1402
+ switch (request) {
1403
+ case EchoRequests.MAINTENANCE:
1404
+ this.maintenanceMode();
1405
+ break;
1406
+ case EchoRequests.REGISTRATION:
1407
+ this.followRegistrationLink(code, user);
1408
+ break;
1409
+ case EchoRequests.INVITATION:
1410
+ this.followInvitationLink(code);
1411
+ break;
1412
+ case EchoRequests.EMAIL_CHANGE:
1413
+ this.followEmailChangeConfirmationLink(code);
1414
+ break;
1415
+ case EchoRequests.GITHUB_INTEGRATION:
1416
+ this.endGitHubIntegrationFlow(code === 'done');
1417
+ break;
1418
+ case EchoRequests.TRELLO_INTEGRATION:
1419
+ this.endTrelloIntegrationFlow(code);
1420
+ break;
1421
+ case EchoRequests.MICROSOFT_CALENDARS_INTEGRATION:
1422
+ this.endExternalCalendarsIntegrationFlow(ExternalCalendarSources.MICROSOFT, code, state);
1423
+ break;
1424
+ case EchoRequests.GOOGLE_CALENDARS_INTEGRATION:
1425
+ this.endExternalCalendarsIntegrationFlow(ExternalCalendarSources.GOOGLE, code, state);
1426
+ break;
1427
+ default:
1428
+ this.success = false;
1429
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.INVALID_ACTION');
1430
+ break;
1431
+ }
1432
+ }
1433
+ maintenanceMode() {
1434
+ const announcement = this._tc.get('idea-announcement');
1435
+ if (announcement && announcement.content)
1436
+ this.htmlContent = mdToHtml(announcement.content);
1437
+ }
1438
+ async followRegistrationLink(code, user) {
1439
+ try {
1440
+ await this._loading.show();
1441
+ await this._API.postResource('cognito2', {
1442
+ idea: true,
1443
+ body: {
1444
+ action: 'CONFIRM_SIGN_UP',
1445
+ username: user,
1446
+ confirmationCode: code,
1447
+ cognitoUserPoolClientId: this._env.aws.cognito.userPoolClientId
1448
+ }
1449
+ });
1450
+ this.success = true;
1451
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.ACCOUNT_CONFIRMED');
1452
+ }
1453
+ catch (error) {
1454
+ this.success = false;
1455
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.REQUEST_FAILED');
1456
+ }
1457
+ finally {
1458
+ this._loading.hide();
1459
+ }
1460
+ }
1461
+ async followInvitationLink(code) {
1462
+ try {
1463
+ await this._loading.show();
1464
+ await this._API.getResource('invitations', { idea: true, resourceId: code });
1465
+ this.success = true;
1466
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.TEAM_JOINED');
1467
+ }
1468
+ catch (error) {
1469
+ this.success = false;
1470
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.REQUEST_FAILED');
1471
+ }
1472
+ finally {
1473
+ this._loading.hide();
1474
+ }
1475
+ }
1476
+ async followEmailChangeConfirmationLink(code) {
1477
+ try {
1478
+ await this._loading.show();
1479
+ await this._API.getResource('emailChangeRequests', {
1480
+ idea: true,
1481
+ resourceId: code,
1482
+ params: { project: this._env.idea.project }
1483
+ });
1484
+ this.success = true;
1485
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.EMAIL_CHANGED');
1486
+ }
1487
+ catch (error) {
1488
+ this.success = false;
1489
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.REQUEST_FAILED');
1490
+ }
1491
+ finally {
1492
+ this._loading.hide();
1493
+ }
1494
+ }
1495
+ endGitHubIntegrationFlow(success) {
1496
+ this.success = success;
1497
+ this.content = success
1498
+ ? this._translate._('IDEA_UNCOMMON.ECHO.GITHUB_SOURCE_INTEGRATION_SUCCESS')
1499
+ : this._translate._('IDEA_UNCOMMON.ECHO.GITHUB_SOURCE_INTEGRATION_ERROR');
1500
+ }
1501
+ endTrelloIntegrationFlow(token) {
1502
+ this.success = Boolean(token);
1503
+ this.content = token
1504
+ ? this._translate._('IDEA_UNCOMMON.ECHO.TRELLO_SOURCE_INTEGRATION_SUCCESS', { token })
1505
+ : this._translate._('IDEA_UNCOMMON.ECHO.TRELLO_SOURCE_INTEGRATION_ERROR');
1506
+ }
1507
+ async endExternalCalendarsIntegrationFlow(service, code, calendarId) {
1508
+ try {
1509
+ await this._loading.show();
1510
+ await this._API.patchResource('calendars', {
1511
+ idea: true,
1512
+ body: { action: 'SET_EXTERNAL_INTEGRATION', service, code, calendarId, project: this._env.idea.project }
1513
+ });
1514
+ this.success = true;
1515
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.EXTERNAL_CALENDARS_SOURCE_INTEGRATION_SUCCESS');
1516
+ }
1517
+ catch (error) {
1518
+ this.success = false;
1519
+ this.content = this._translate._('IDEA_UNCOMMON.ECHO.EXTERNAL_CALENDARS_SOURCE_INTEGRATION_ERROR');
1520
+ }
1521
+ finally {
1522
+ this._loading.hide();
1523
+ }
1524
+ }
1525
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1526
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAEchoPage, selector: "idea-echo", ngImport: i0, template: "<ion-content>\n <div class=\"flexBox\">\n <ion-card class=\"echoCard maxWidthContainer\">\n <ion-img [src]=\"'assets/icons/icon-auth.svg'\" />\n <ion-card-header>\n <ion-card-title>\n @if (success === true) {\n <ion-icon color=\"success\" name=\"checkmark\" />\n } @if (success === false) {\n <ion-icon color=\"danger\" name=\"close\" />\n }\n </ion-card-title>\n </ion-card-header>\n <ion-card-content class=\"selectable\">\n {{ content }} @if (htmlContent) {\n <div class=\"htmlContent\" [innerHTML]=\"htmlContent\"></div>\n } @if (success === false || success === true) {\n <p class=\"youCanCloseMe\">\n <i>{{ 'IDEA_UNCOMMON.ECHO.YOU_CAN_CLOSE_THE_PAGE' | translate }}</i>\n </p>\n }\n </ion-card-content>\n </ion-card>\n </div>\n</ion-content>\n", styles: [".echoCard{min-width:300px;padding:30px 20px;text-align:center}.echoCard ion-img{width:80px;display:block;margin:0 auto}.echoCard ion-icon{font-size:1.8em}.echoCard ion-card-content{margin-bottom:20px;font-size:1.2em}.echoCard ion-card-content p.youCanCloseMe{margin-top:12px}\n"], dependencies: [{ kind: "component", type: i1$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i1$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i1$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
1527
+ }
1528
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoPage, decorators: [{
1529
+ type: Component,
1530
+ args: [{ selector: 'idea-echo', template: "<ion-content>\n <div class=\"flexBox\">\n <ion-card class=\"echoCard maxWidthContainer\">\n <ion-img [src]=\"'assets/icons/icon-auth.svg'\" />\n <ion-card-header>\n <ion-card-title>\n @if (success === true) {\n <ion-icon color=\"success\" name=\"checkmark\" />\n } @if (success === false) {\n <ion-icon color=\"danger\" name=\"close\" />\n }\n </ion-card-title>\n </ion-card-header>\n <ion-card-content class=\"selectable\">\n {{ content }} @if (htmlContent) {\n <div class=\"htmlContent\" [innerHTML]=\"htmlContent\"></div>\n } @if (success === false || success === true) {\n <p class=\"youCanCloseMe\">\n <i>{{ 'IDEA_UNCOMMON.ECHO.YOU_CAN_CLOSE_THE_PAGE' | translate }}</i>\n </p>\n }\n </ion-card-content>\n </ion-card>\n </div>\n</ion-content>\n", styles: [".echoCard{min-width:300px;padding:30px 20px;text-align:center}.echoCard ion-img{width:80px;display:block;margin:0 auto}.echoCard ion-icon{font-size:1.8em}.echoCard ion-card-content{margin-bottom:20px;font-size:1.2em}.echoCard ion-card-content p.youCanCloseMe{margin-top:12px}\n"] }]
1531
+ }] });
1532
+ var EchoRequests;
1533
+ (function (EchoRequests) {
1534
+ EchoRequests["MAINTENANCE"] = "maintenance";
1535
+ EchoRequests["REGISTRATION"] = "registration";
1536
+ EchoRequests["INVITATION"] = "invitation";
1537
+ EchoRequests["EMAIL_CHANGE"] = "email-change";
1538
+ EchoRequests["GITHUB_INTEGRATION"] = "github-integration";
1539
+ EchoRequests["TRELLO_INTEGRATION"] = "trello-integration";
1540
+ EchoRequests["MICROSOFT_CALENDARS_INTEGRATION"] = "microsoft-calendars-integration";
1541
+ EchoRequests["GOOGLE_CALENDARS_INTEGRATION"] = "google-calendars-integration";
1542
+ })(EchoRequests || (EchoRequests = {}));
1543
+
1544
+ const routes = [
1545
+ { path: '', component: IDEAEchoPage },
1546
+ { path: ':request', component: IDEAEchoPage }
1547
+ ];
1548
+ class IDEAEchoModule {
1549
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1550
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoModule, declarations: [IDEAEchoPage], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, i1$2.RouterModule] }); }
1551
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, RouterModule.forChild(routes)] }); }
1552
+ }
1553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAEchoModule, decorators: [{
1554
+ type: NgModule,
1555
+ args: [{
1556
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule, RouterModule.forChild(routes)],
1557
+ declarations: [IDEAEchoPage]
1558
+ }]
1559
+ }] });
1560
+
1561
+ class IDEAMDEToolbarComponent {
1562
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1563
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.1", type: IDEAMDEToolbarComponent, selector: "idea-mde-toolbar", ngImport: i0, template: "<div class=\"mdeToolbar\">\n <b>**{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.BOLD' | translate }}**</b>\n <i>*{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.ITALIC' | translate }}*</i>\n ~{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.STRIKE' | translate }}~\n</div>\n", styles: [".mdeToolbar{text-align:right;color:#aaa;padding:5px 0;font-size:.9em}.mdeToolbar b,.mdeToolbar i{padding-right:5px}\n"], dependencies: [{ kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
1564
+ }
1565
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEToolbarComponent, decorators: [{
1566
+ type: Component,
1567
+ args: [{ selector: 'idea-mde-toolbar', template: "<div class=\"mdeToolbar\">\n <b>**{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.BOLD' | translate }}**</b>\n <i>*{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.ITALIC' | translate }}*</i>\n ~{{ 'IDEA_UNCOMMON.MDE.TOOLBAR.STRIKE' | translate }}~\n</div>\n", styles: [".mdeToolbar{text-align:right;color:#aaa;padding:5px 0;font-size:.9em}.mdeToolbar b,.mdeToolbar i{padding-right:5px}\n"] }]
1568
+ }] });
1569
+
1570
+ class IDEAMDEComponent {
1571
+ constructor() {
1572
+ this._modal = inject(ModalController);
1573
+ }
1574
+ ngOnInit() {
1575
+ this.id = this.id || 'mde';
1576
+ this.variables = this.variables || new Array();
1577
+ this.value = this.initialValue;
1578
+ }
1579
+ close() {
1580
+ this._modal.dismiss();
1581
+ }
1582
+ confirm() {
1583
+ this._modal.dismiss(this.value);
1584
+ }
1585
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1586
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAMDEComponent, selector: "idea-mde", inputs: { id: "id", title: "title", header: "header", description: "description", variables: "variables", initialValue: "initialValue", placeholder: "placeholder" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'IDEA_UNCOMMON.MDE.CLOSE_WITHOUT_SAVING' | translate\" (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>\n {{ title || ('IDEA_UNCOMMON.MDE.TITLE' | translate) }}\n </ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'IDEA_UNCOMMON.MDE.SAVE_AND_CLOSE' | translate\" (click)=\"confirm()\">\n <ion-icon name=\"checkmark\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content class=\"ion-padding\">\n @if (header) {\n <h2 class=\"header\">\n {{ header }}\n </h2>\n }\n @if (description) {\n <p class=\"description\">\n {{ description }}\n </p>\n }\n @if (variables?.length) {\n <p class=\"variables\">\n <b> {{ 'IDEA_UNCOMMON.MDE.VARIABLES' | translate }}: </b>\n @for (v of variables; track v) {\n <ion-text>\n {{ v.concat(' ') }}\n </ion-text>\n }\n </p>\n }\n <ion-textarea rows=\"12\" [id]=\"id\" [placeholder]=\"placeholder\" [(ngModel)]=\"value\" />\n <idea-mde-toolbar />\n</ion-content>\n", styles: ["ion-content{--ion-background-color: #f4f4f4}h2.header{margin:0;font-size:.7em;font-weight:700;text-transform:uppercase}.description{margin-top:5px;margin-bottom:20px;font-size:.9em}.variables{margin:10px 5px;font-size:.8em}ion-textarea{--background: var(--ion-color-white);padding:10px}\n"], dependencies: [{ kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i1$1.IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: i1$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i1$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TextValueAccessor, selector: "ion-input:not([type=number]),ion-textarea,ion-searchbar" }, { kind: "component", type: IDEAMDEToolbarComponent, selector: "idea-mde-toolbar" }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
1587
+ }
1588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEComponent, decorators: [{
1589
+ type: Component,
1590
+ args: [{ selector: 'idea-mde', template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'IDEA_UNCOMMON.MDE.CLOSE_WITHOUT_SAVING' | translate\" (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>\n {{ title || ('IDEA_UNCOMMON.MDE.TITLE' | translate) }}\n </ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'IDEA_UNCOMMON.MDE.SAVE_AND_CLOSE' | translate\" (click)=\"confirm()\">\n <ion-icon name=\"checkmark\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content class=\"ion-padding\">\n @if (header) {\n <h2 class=\"header\">\n {{ header }}\n </h2>\n }\n @if (description) {\n <p class=\"description\">\n {{ description }}\n </p>\n }\n @if (variables?.length) {\n <p class=\"variables\">\n <b> {{ 'IDEA_UNCOMMON.MDE.VARIABLES' | translate }}: </b>\n @for (v of variables; track v) {\n <ion-text>\n {{ v.concat(' ') }}\n </ion-text>\n }\n </p>\n }\n <ion-textarea rows=\"12\" [id]=\"id\" [placeholder]=\"placeholder\" [(ngModel)]=\"value\" />\n <idea-mde-toolbar />\n</ion-content>\n", styles: ["ion-content{--ion-background-color: #f4f4f4}h2.header{margin:0;font-size:.7em;font-weight:700;text-transform:uppercase}.description{margin-top:5px;margin-bottom:20px;font-size:.9em}.variables{margin:10px 5px;font-size:.8em}ion-textarea{--background: var(--ion-color-white);padding:10px}\n"] }]
1591
+ }], propDecorators: { id: [{
1592
+ type: Input
1593
+ }], title: [{
1594
+ type: Input
1595
+ }], header: [{
1596
+ type: Input
1597
+ }], description: [{
1598
+ type: Input
1599
+ }], variables: [{
1600
+ type: Input
1601
+ }], initialValue: [{
1602
+ type: Input
1603
+ }], placeholder: [{
1604
+ type: Input
1605
+ }] } });
1606
+
1607
+ class IDEAMDEModule {
1608
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1609
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEModule, declarations: [IDEAMDEComponent, IDEAMDEToolbarComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEAMDEComponent, IDEAMDEToolbarComponent] }); }
1610
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
1611
+ }
1612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAMDEModule, decorators: [{
1613
+ type: NgModule,
1614
+ args: [{
1615
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
1616
+ declarations: [IDEAMDEComponent, IDEAMDEToolbarComponent],
1617
+ exports: [IDEAMDEComponent, IDEAMDEToolbarComponent]
1618
+ }]
1619
+ }] });
1620
+
1621
+ /**
1622
+ * After how much time we need to request a synchronisation to keep the contents up-to-date.
1623
+ */
1624
+ const SYNC_EXPIRATION_INTERVAL = 1000 * 60 * 60 * 24; // a day
1625
+ /**
1626
+ * Prefix for the key of the API requests queue info in the local storage.
1627
+ */
1628
+ const QUEUE_API_REQUESTS_KEY = 'IDEAOfflineService.queueAPIrequests';
1629
+ /**
1630
+ * Prefix for the key of the last sync time info in the local storage.
1631
+ */
1632
+ const LAST_SYNC_KEY = 'IDEAOfflineService.lastSyncAt';
1633
+ /**
1634
+ * The max number of elements per resource to synchronize during the first analysis of a synchronisation.
1635
+ * If the number of elements exceeds (there is more data after the 1st page), the operation require manual confirmation.
1636
+ */
1637
+ const NUM_ELEMENTS_WITHOUT_MANUAL_SYNC = 100;
1638
+ /**
1639
+ * The max number of elements per resource to request at the same time during a normal synchronisation.
1640
+ */
1641
+ const NUM_ELEMENTS_WITH_MANUAL_SYNC = 300;
1642
+ /**
1643
+ * Manage the offline functioning of the app's data (upload/download/synchronization).
1644
+ * Note: it includes all the methods of IDEAOfflineService.
1645
+ *
1646
+ * Download: it works through the **Delta** mechanism on back-end resources.
1647
+ * Practically, the service prepares each possible API GET requests for each of the involved resources.
1648
+ * In this way, if offline, the API service can hit an element by URL in the cache (storage offline).
1649
+ *
1650
+ * **How to use it**. Configure the sync mechanism in the main app.component as it follows:
1651
+ *
1652
+ * 1. Implement a CacheableResource class for each DeltaResource you want to cache offline.
1653
+ * 2. Set up the service (`setUpOfflineData`).
1654
+ * 3. Run a synchronization.
1655
+ * 4. Add the navigation to the Offline Manager (UI).
1656
+ * 5. Optionally, push offline API requests to the queue when offline.
1657
+ *
1658
+ * e.g. of CacheableResource classes:
1659
+ * ```
1660
+ * class CustomerCR extends CacheableResource {
1661
+ * constructor(description: string) {
1662
+ * super(DeltaResources.CUSTOMERS, description);
1663
+ * }
1664
+ * public listURL(element: any): string {
1665
+ * return `teams/${element.teamId}/customers`;
1666
+ * }
1667
+ * public elementURL(element: any): string {
1668
+ * return `teams/${element.teamId}/customers/${element.customerId}`;
1669
+ * }
1670
+ * public findIndexInList(list: Array<any>, element: any): number {
1671
+ * return list.findIndex(x => x.customerId === element.customerId);
1672
+ * }
1673
+ * public sort(a: any, b: any): number {
1674
+ * return a.name.localeCompare(b.name);
1675
+ * }
1676
+ * }
1677
+ */
1678
+ class IDEAOfflineDataService {
1679
+ constructor() {
1680
+ this._storage = inject(IDEAStorageService);
1681
+ this._translate = inject(IDEATranslationsService);
1682
+ this._offline = inject(IDEAOfflineService);
1683
+ this._API = inject(IDEAAWSAPIService);
1684
+ this.queueAPIRequests = new Array();
1685
+ this.resources = new Array();
1686
+ this.cacheableResources = {};
1687
+ // subscribe to network changes
1688
+ this._offline.subscribe(isOnline => {
1689
+ if (isOnline) {
1690
+ // try a synchronization if needed or if the last one failed
1691
+ if (this.errorInLastSync)
1692
+ this.synchronize();
1693
+ else
1694
+ this.synchronizeIfNeeded();
1695
+ }
1696
+ });
1697
+ }
1698
+ //
1699
+ // NETWORK STATUS from IDEAOfflineService
1700
+ //
1701
+ /**
1702
+ * Quickly check the connection status.
1703
+ */
1704
+ isOnline() {
1705
+ return this._offline.isOnline();
1706
+ }
1707
+ /**
1708
+ * Quickly check the connection status.
1709
+ */
1710
+ isOffline() {
1711
+ return this._offline.isOffline();
1712
+ }
1713
+ /**
1714
+ * Subscribe to the service to be notified when the connection status changes.
1715
+ */
1716
+ subscribe(callback) {
1717
+ return this._offline.subscribe(callback);
1718
+ }
1719
+ /**
1720
+ * Quickly check for online connection.
1721
+ */
1722
+ check() {
1723
+ return this._offline.check();
1724
+ }
1725
+ //
1726
+ // CONFIGURATION
1727
+ //
1728
+ /**
1729
+ * Whether the offline mode is allowed. You can customize this function, if needed.
1730
+ */
1731
+ isAllowed() {
1732
+ return !!this.resources.length;
1733
+ }
1734
+ /**
1735
+ * Set up the service to use and sync offline data.
1736
+ */
1737
+ setUpOfflineData(teamId, useQueueAPIRequests, cacheableResources) {
1738
+ // set the team
1739
+ this.teamId = teamId;
1740
+ // decide if to allow certain API request while offline
1741
+ this.useQueueAPIRequests = useQueueAPIRequests;
1742
+ // load the API request pending queue, if any
1743
+ this.queueAPIRequestKey = this.teamId ? `${QUEUE_API_REQUESTS_KEY}_${this.teamId}` : QUEUE_API_REQUESTS_KEY;
1744
+ this.loadQueueAPIRequest();
1745
+ // load the lastSyncAt information for the team
1746
+ this.lastSyncKey = this.teamId ? `${LAST_SYNC_KEY}_${this.teamId}` : LAST_SYNC_KEY;
1747
+ this.loadLastSyncAt();
1748
+ // load the cacheable resources
1749
+ this.resources = new Array();
1750
+ this.cacheableResources = {};
1751
+ cacheableResources.forEach(r => this.addCacheableResource(r));
1752
+ }
1753
+ /**
1754
+ * Load from the local storage the last sync at information.
1755
+ */
1756
+ loadLastSyncAt() {
1757
+ return new Promise(resolve => {
1758
+ this._storage.get(this.lastSyncKey).then((lastSyncAt) => {
1759
+ this.lastSyncAt = lastSyncAt ? Number(lastSyncAt) : null;
1760
+ resolve(this.lastSyncAt);
1761
+ });
1762
+ });
1763
+ }
1764
+ /**
1765
+ * Update last sync at info in the local storage.
1766
+ */
1767
+ saveLastSyncAt(lastSyncAt) {
1768
+ this.lastSyncAt = lastSyncAt;
1769
+ return this._storage.set(this.lastSyncKey, lastSyncAt);
1770
+ }
1771
+ /**
1772
+ * Add a CacheableResource.
1773
+ */
1774
+ addCacheableResource(cacheableResource) {
1775
+ if (this.teamId)
1776
+ cacheableResource.teamId = this.teamId;
1777
+ this.cacheableResources[cacheableResource.resource] = cacheableResource;
1778
+ this.resources.push(cacheableResource.resource);
1779
+ }
1780
+ /**
1781
+ * Remove a CacheableResource by its DeltaResource.
1782
+ */
1783
+ removeCacheableResource(resource) {
1784
+ delete this.cacheableResources[resource];
1785
+ this.resources.splice(this.resources.indexOf(resource), 1);
1786
+ }
1787
+ /**
1788
+ * Get the the DeltaResourced configured.
1789
+ */
1790
+ getResources() {
1791
+ return this.resources;
1792
+ }
1793
+ /**
1794
+ * Get a CacheableResource by its DeltaResource.
1795
+ */
1796
+ getCacheableResource(resource) {
1797
+ return this.cacheableResources[resource];
1798
+ }
1799
+ //
1800
+ // UPLOAD
1801
+ //
1802
+ /**
1803
+ * Execute all the API requests in the queue; the requests terminated with an error will remain in the queue.
1804
+ */
1805
+ runQueueAPIRequests() {
1806
+ return new Promise((resolve, reject) => {
1807
+ if (!this.useQueueAPIRequests || this.isOffline() || !this.queueAPIRequests.length)
1808
+ return resolve();
1809
+ this.queueAPIRequests.forEach(async (request) => {
1810
+ const options = {};
1811
+ if (request.resourceId)
1812
+ options.resourceId = request.resourceId;
1813
+ if (request.body)
1814
+ options.body = request.body;
1815
+ let promise;
1816
+ try {
1817
+ switch (request.method.toUpperCase()) {
1818
+ case 'POST':
1819
+ promise = await this._API.postResource(request.resource, options);
1820
+ break;
1821
+ case 'PUT':
1822
+ promise = await this._API.putResource(request.resource, options);
1823
+ break;
1824
+ case 'PATCH':
1825
+ promise = await this._API.patchResource(request.resource, options);
1826
+ break;
1827
+ }
1828
+ if (!promise)
1829
+ request.error = 'INVALID_METHOD';
1830
+ else
1831
+ request.error = null;
1832
+ }
1833
+ catch (err) {
1834
+ request.error = err.message || 'UNKNOWN_ERROR';
1835
+ }
1836
+ });
1837
+ // keep the requests NOT successfully executed
1838
+ this.saveQueueAPIRequest(this.queueAPIRequests.filter(x => x.error)).then(() => {
1839
+ // if the queue is empty, the entire operation was successful
1840
+ if (this.queueAPIRequests.length)
1841
+ reject();
1842
+ else
1843
+ resolve();
1844
+ });
1845
+ });
1846
+ }
1847
+ /**
1848
+ * Load from the local storage the API requests queue.
1849
+ */
1850
+ loadQueueAPIRequest() {
1851
+ return new Promise(resolve => {
1852
+ if (!this.useQueueAPIRequests)
1853
+ return resolve([]);
1854
+ this._storage.get(this.queueAPIRequestKey).then((queue) => {
1855
+ this.queueAPIRequests = queue || [];
1856
+ resolve(this.queueAPIRequests);
1857
+ });
1858
+ });
1859
+ }
1860
+ /**
1861
+ * Update the queue in memory and also save the copy in the local storage.
1862
+ */
1863
+ saveQueueAPIRequest(queue) {
1864
+ if (!this.isAllowed())
1865
+ return Promise.reject();
1866
+ this.queueAPIRequests = queue || this.queueAPIRequests;
1867
+ return this._storage.set(this.queueAPIRequestKey, this.queueAPIRequests);
1868
+ }
1869
+ /**
1870
+ * Delete a request stuck in an error.
1871
+ */
1872
+ deleteRequest(request) {
1873
+ if (!this.isAllowed())
1874
+ return Promise.reject();
1875
+ if (request) {
1876
+ this.queueAPIRequests.splice(this.queueAPIRequests.indexOf(request), 1);
1877
+ this.saveQueueAPIRequest();
1878
+ }
1879
+ }
1880
+ //
1881
+ // DOWNLOAD
1882
+ //
1883
+ /**
1884
+ * Save the records of a delta and, if the latter has more pages, go recursive.
1885
+ */
1886
+ async syncDeltaRecords(delta, done) {
1887
+ // for each resource, we reset the status; if a resource still need to be synchronized, it will be flagged below
1888
+ this.resources.forEach(resource => {
1889
+ this.cacheableResources[resource].error = false;
1890
+ this.cacheableResources[resource].synchronizing = false;
1891
+ });
1892
+ try {
1893
+ // synchronze each of the resources included in the delta; this execution can go in parallel
1894
+ await Promise.all(delta.resources.map(async (resource) => {
1895
+ const cr = this.cacheableResources[resource];
1896
+ if (cr) {
1897
+ // synchronize the delta records; note: the executuion inside a resource must go in series
1898
+ cr.synchronizing = true;
1899
+ const success = await this.syncResourceDeltaRecords(resource, delta.records[resource]);
1900
+ if (!success)
1901
+ this.errorInLastSync = true;
1902
+ }
1903
+ }));
1904
+ // if there were error of if there is nothing left to sync, we are done
1905
+ if (this.errorInLastSync || !delta.next)
1906
+ return done();
1907
+ // otherwise, get the next page of the delta and go recursive until we are done
1908
+ const params = { next: delta.next, limit: NUM_ELEMENTS_WITH_MANUAL_SYNC };
1909
+ if (this.lastSyncAt)
1910
+ params.since = this.lastSyncAt;
1911
+ const newDelta = await this._API.getResource(this.teamId ? `teams/${this.teamId}/delta` : 'delta', { params });
1912
+ this.syncDeltaRecords(newDelta, done);
1913
+ }
1914
+ catch (err) {
1915
+ // if something went wrong, stops the operation, since we need to make sure the entire flow is consistent
1916
+ this.errorInLastSync = true;
1917
+ return done();
1918
+ }
1919
+ }
1920
+ /**
1921
+ * Save offline a list of DeltaRecords for this resource, creating a map of all the corresponding API GET requests.
1922
+ */
1923
+ async syncResourceDeltaRecords(resource, records) {
1924
+ // acquire the info an a cacheable resource
1925
+ const cr = this.cacheableResources[resource];
1926
+ if (!cr)
1927
+ return Promise.resolve(true);
1928
+ try {
1929
+ // prepare helpers to group executions of elements of the same list
1930
+ let list = null, oldListURL = null;
1931
+ for (const r of records) {
1932
+ // calculate the URL to access the API request key for this element
1933
+ const elementURL = cr.elementURL(r.element);
1934
+ // if the element was deleted (based on delta), delete its corresponding API GET request
1935
+ if (r.deleted)
1936
+ await this._API.deleteFromCache(elementURL);
1937
+ // otherwise, update it
1938
+ else
1939
+ await this._API.putInCache(elementURL, r.element);
1940
+ // calculate the URLs to access the API request key for this element's list
1941
+ const listURL = cr.listURL(r.element);
1942
+ // to improve the performance, get/sort/save the list only when it changes from the previous element's one.
1943
+ if (listURL !== oldListURL) {
1944
+ // skip first cycle (oldListURL === null)
1945
+ if (oldListURL) {
1946
+ // re-sort the list; note: it should be sorted from the last time, so we only manage the new element
1947
+ list = list.sort(cr.sort);
1948
+ // save the updated list
1949
+ await this._API.putInCache(oldListURL, list);
1950
+ }
1951
+ // get the list of elements from the local storage by the calculated URL
1952
+ list = await this._API.getFromCache(listURL);
1953
+ // save the reference to the list for the next cycle
1954
+ oldListURL = listURL;
1955
+ }
1956
+ // find in the list the element we want to manage
1957
+ const index = cr.findIndexInList(list, r.element);
1958
+ // delete the element from the list
1959
+ if (index !== -1)
1960
+ list.splice(index, 1);
1961
+ // if the element wasn't deleted (following the delta record information), insert the new version
1962
+ if (!r.deleted)
1963
+ list.push(r.element);
1964
+ }
1965
+ // in case there were elements, save the last element's list
1966
+ if (oldListURL) {
1967
+ // re-sort the list; note: it should be sorted from the last time, so we only manage the new element
1968
+ list = list.sort(cr.sort);
1969
+ // save the updated list
1970
+ await this._API.putInCache(oldListURL, list);
1971
+ }
1972
+ return Promise.resolve(true);
1973
+ }
1974
+ catch (err) {
1975
+ // if something went wrong, stops the operation, since we need to make sure the entire flow is consistent
1976
+ cr.error = true;
1977
+ return Promise.resolve(false);
1978
+ }
1979
+ }
1980
+ //
1981
+ // SYNCHRONISATION (UPLOAD+DOWNLOAD)
1982
+ //
1983
+ /**
1984
+ * Run a sync, but only if needed, i.e. the cached content expired or we have pending API requests in the queue.
1985
+ */
1986
+ synchronizeIfNeeded() {
1987
+ return new Promise((resolve, reject) => {
1988
+ if (!this.isAllowed())
1989
+ return;
1990
+ this.loadLastSyncAt().then(() => this.loadQueueAPIRequest()
1991
+ .then(() => {
1992
+ if (this.queueAPIRequests.length || Date.now() > this.lastSyncAt + SYNC_EXPIRATION_INTERVAL)
1993
+ this.synchronize();
1994
+ resolve();
1995
+ })
1996
+ .catch(err => reject(err)));
1997
+ });
1998
+ }
1999
+ /**
2000
+ * Synchronization of the whole set of resources (upload+download).
2001
+ * @param manualConfirmation if true, the request comes directly from a user action (not automatic procedures).
2002
+ */
2003
+ synchronize(manualConfirmation) {
2004
+ return new Promise((resolve, reject) => {
2005
+ if (!this.isAllowed() || this.synchronizing)
2006
+ return resolve();
2007
+ this.synchronizing = true;
2008
+ this.errorInLastSync = false;
2009
+ this.requiresManualConfirmation = false;
2010
+ // load the lastSyncAt information
2011
+ this.loadLastSyncAt().then(() =>
2012
+ // try to run all the pending API requests in the queue, i.e. uploading the offline resources changed
2013
+ this.runQueueAPIRequests()
2014
+ .then(() => {
2015
+ // set this moment in time: if anything happened before the end of the sync, we don't lose fresh data
2016
+ const now = Date.now();
2017
+ // prepare a first "short" Delta request, to see if there is a lot of data to process
2018
+ const params = { limit: NUM_ELEMENTS_WITHOUT_MANUAL_SYNC };
2019
+ if (this.lastSyncAt)
2020
+ params.since = this.lastSyncAt;
2021
+ this._API
2022
+ .getResource(this.teamId ? `teams/${this.teamId}/delta` : 'delta', { params })
2023
+ .then((delta) => {
2024
+ // decide if to proceed with the sync: in case there is another page, it requires a manual confirmation
2025
+ if (delta.next && !manualConfirmation) {
2026
+ this.requiresManualConfirmation = true;
2027
+ this.synchronizing = false;
2028
+ return resolve();
2029
+ }
2030
+ // save the records in the delta and request possible new pages
2031
+ this.syncDeltaRecords(delta, () => {
2032
+ if (this.errorInLastSync) {
2033
+ this.synchronizing = false;
2034
+ resolve();
2035
+ }
2036
+ else {
2037
+ // all the resources are succesfully in sync
2038
+ this.resources.forEach(resource => (this.cacheableResources[resource].synchronizing = false));
2039
+ // update the timestamp of last sync
2040
+ this.saveLastSyncAt(now)
2041
+ .then(() => {
2042
+ this.synchronizing = false;
2043
+ resolve();
2044
+ })
2045
+ .catch(() => {
2046
+ // we couldn't save the last sync info
2047
+ this.errorInLastSync = true;
2048
+ this.synchronizing = false;
2049
+ reject();
2050
+ });
2051
+ }
2052
+ });
2053
+ })
2054
+ .catch(() => {
2055
+ // we couldn't acquire new information (Delta)
2056
+ this.errorInLastSync = true;
2057
+ this.synchronizing = false;
2058
+ reject();
2059
+ });
2060
+ })
2061
+ .catch(() => {
2062
+ // we stop, since we don't want backend data to override local info not yet uploaded
2063
+ this.errorInLastSync = true;
2064
+ this.synchronizing = false;
2065
+ reject();
2066
+ }));
2067
+ });
2068
+ }
2069
+ /**
2070
+ * Force a full synchronisation.
2071
+ */
2072
+ forceFullSync() {
2073
+ this._storage.clear().then(() => this.synchronize(true));
2074
+ }
2075
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2076
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineDataService }); }
2077
+ }
2078
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineDataService, decorators: [{
2079
+ type: Injectable
2080
+ }], ctorParameters: () => [] });
2081
+ /**
2082
+ * The abstract class to implement for each resource to cache offline from the back-end (DOWNLOAD).
2083
+ */
2084
+ class CacheableResource {
2085
+ constructor(resource, description) {
2086
+ this.resource = resource;
2087
+ this.description = description;
2088
+ this.synchronizing = false;
2089
+ this.error = false;
2090
+ // loaded at runtime
2091
+ this.teamId = null;
2092
+ }
2093
+ }
2094
+
2095
+ class IDEACacheableResourceComponent {
2096
+ constructor() {
2097
+ this._offline = inject(IDEAOfflineDataService);
2098
+ }
2099
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACacheableResourceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2100
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEACacheableResourceComponent, selector: "idea-cacheable-resource", inputs: { resource: "resource" }, ngImport: i0, template: "<ion-item class=\"cacheableResourceItem\">\n <ion-label>{{ resource.description }}</ion-label>\n @if (resource.error) {\n <ion-icon slot=\"end\" name=\"alert-circle\" color=\"danger\" />\n }\n @if (!resource.error && _offline.synchronizing && resource.synchronizing) {\n <ion-spinner slot=\"end\" name=\"dots\" />\n }\n @if (!resource.error && !resource.synchronizing && !_offline.requiresManualConfirmation) {\n <ion-icon slot=\"end\" name=\"checkmark\" color=\"success\" />\n }\n @if (!resource.error && !resource.synchronizing && _offline.requiresManualConfirmation) {\n <ion-icon slot=\"end\" name=\"pause\" color=\"dark\" />\n }\n</ion-item>\n", styles: [""], dependencies: [{ kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }] }); }
2101
+ }
2102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACacheableResourceComponent, decorators: [{
2103
+ type: Component,
2104
+ args: [{ selector: 'idea-cacheable-resource', template: "<ion-item class=\"cacheableResourceItem\">\n <ion-label>{{ resource.description }}</ion-label>\n @if (resource.error) {\n <ion-icon slot=\"end\" name=\"alert-circle\" color=\"danger\" />\n }\n @if (!resource.error && _offline.synchronizing && resource.synchronizing) {\n <ion-spinner slot=\"end\" name=\"dots\" />\n }\n @if (!resource.error && !resource.synchronizing && !_offline.requiresManualConfirmation) {\n <ion-icon slot=\"end\" name=\"checkmark\" color=\"success\" />\n }\n @if (!resource.error && !resource.synchronizing && _offline.requiresManualConfirmation) {\n <ion-icon slot=\"end\" name=\"pause\" color=\"dark\" />\n }\n</ion-item>\n" }]
2105
+ }], propDecorators: { resource: [{
2106
+ type: Input
2107
+ }] } });
2108
+
2109
+ class IDEAOfflineManagerComponent {
2110
+ constructor() {
2111
+ this._modal = inject(ModalController);
2112
+ this._alert = inject(AlertController);
2113
+ this._actions = inject(IDEAActionSheetController);
2114
+ this._translate = inject(IDEATranslationsService);
2115
+ this._offline = inject(IDEAOfflineDataService);
2116
+ }
2117
+ async sync() {
2118
+ const alert = await this._alert.create({
2119
+ header: this._translate._('IDEA_UNCOMMON.OFFLINE.SYNC_NOW'),
2120
+ message: this._translate._('IDEA_UNCOMMON.OFFLINE.DONT_EXIT_APP_DISCLAIMER'),
2121
+ buttons: [
2122
+ { text: this._translate._('COMMON.CANCEL') },
2123
+ { text: this._translate._('COMMON.GOT_IT'), handler: () => this._offline.synchronize(true) }
2124
+ ]
2125
+ });
2126
+ alert.present();
2127
+ }
2128
+ async deleteRequest(request) {
2129
+ const alert = await this._alert.create({
2130
+ header: this._translate._('COMMON.ARE_YOU_SURE'),
2131
+ message: this._translate._('IDEA_UNCOMMON.OFFLINE.DELETION_IS_IRREVERSIBLE'),
2132
+ buttons: [
2133
+ { text: this._translate._('COMMON.CANCEL') },
2134
+ {
2135
+ text: this._translate._('COMMON.CONFIRM'),
2136
+ handler: () => this._offline.deleteRequest(request)
2137
+ }
2138
+ ]
2139
+ });
2140
+ alert.present();
2141
+ }
2142
+ downloadRequestLog(request) {
2143
+ const dataURL = window.URL.createObjectURL(new Blob([JSON.stringify(request)], { type: 'text/json' }));
2144
+ Browser.open({ url: dataURL });
2145
+ }
2146
+ async actions() {
2147
+ const buttons = [
2148
+ {
2149
+ text: this._translate._('IDEA_UNCOMMON.OFFLINE.FORCE_FULL_SYNC'),
2150
+ icon: 'sync',
2151
+ handler: () => this.forceFullSync()
2152
+ },
2153
+ { text: this._translate._('COMMON.CANCEL'), role: 'cancel', icon: 'arrow-undo' }
2154
+ ];
2155
+ const actions = await this._actions.create({
2156
+ header: this._translate._('IDEA_UNCOMMON.OFFLINE.ADVANCED_ACTIONS'),
2157
+ buttons
2158
+ });
2159
+ actions.present();
2160
+ }
2161
+ async forceFullSync() {
2162
+ const alert = await this._alert.create({
2163
+ header: this._translate._('IDEA_UNCOMMON.OFFLINE.FORCE_FULL_SYNC'),
2164
+ subHeader: this._translate._('COMMON.ARE_YOU_SURE'),
2165
+ message: this._translate._('IDEA_UNCOMMON.OFFLINE.FULL_SYNC_DISCLAIMER'),
2166
+ buttons: [
2167
+ { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
2168
+ { text: this._translate._('COMMON.CONFIRM'), handler: () => this._offline.forceFullSync() }
2169
+ ]
2170
+ });
2171
+ alert.present();
2172
+ }
2173
+ close() {
2174
+ this._modal.dismiss();
2175
+ }
2176
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2177
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAOfflineManagerComponent, selector: "idea-offline-manager", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>\n {{ 'IDEA_UNCOMMON.OFFLINE.OFFLINE_MANAGER' | translate }}\n </ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'IDEA_UNCOMMON.OFFLINE.ADVANCED_ACTIONS' | translate\" (click)=\"actions()\">\n <ion-icon slot=\"icon-only\" name=\"ellipsis-vertical\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-list class=\"aList\">\n @if (!_offline.synchronizing && _offline.requiresManualConfirmation) {\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.HUGE_SYNC_NEED_MANUAL_CONFIRMATION' | translate }}</h2>\n </ion-label>\n </ion-list-header>\n }\n @if (!_offline.synchronizing) {\n <ion-list-header>\n <ion-label class=\"ion-text-wrap\">\n <h3 class=\"lastSyncAt\">\n {{ 'IDEA_UNCOMMON.OFFLINE.LAST_SYNC' | translate }}:\n <ion-text>\n {{\n _offline.lastSyncAt\n ? (_offline.lastSyncAt | dateLocale: 'medium')\n : ('IDEA_UNCOMMON.OFFLINE.NEVER' | translate)\n }}\n </ion-text>\n </h3>\n @if (_offline.errorInLastSync) {\n <p>\n <ion-text color=\"danger\">\n <b>{{ 'IDEA_UNCOMMON.OFFLINE.ERROR_DURING_LAST_SYNC' | translate }}</b>\n </ion-text>\n </p>\n }\n </ion-label>\n <ion-button\n size=\"small\"\n fill=\"solid\"\n [color]=\"_offline.errorInLastSync ? 'danger' : 'dark'\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"sync()\"\n >\n <ion-icon slot=\"start\" name=\"sync\" /> {{ 'IDEA_UNCOMMON.OFFLINE.SYNC_NOW' | translate }}\n </ion-button>\n </ion-list-header>\n }\n </ion-list>\n @if (_offline.synchronizing) {\n <ion-list class=\"aList\">\n <ion-item>\n <ion-label class=\"ion-text-center\">\n <ion-spinner name=\"crescent\" color=\"primary\" />\n </ion-label>\n </ion-item>\n </ion-list>\n }\n @if (_offline.useQueueAPIRequests) {\n <ion-list class=\"aList\">\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.UPLOAD' | translate }}</h2>\n <p>{{ 'IDEA_UNCOMMON.OFFLINE.UPLOAD_QUEUE_REQUESTS_EXPLANATION' | translate }}</p>\n </ion-label>\n </ion-list-header>\n @if (!_offline.queueAPIRequests.length) {\n <ion-item>\n <i>{{ 'IDEA_UNCOMMON.OFFLINE.NO_ELEMENTS_TO_UPLOAD' | translate }}</i>\n <ion-icon slot=\"end\" size=\"small\" name=\"checkmark\" color=\"success\" />\n </ion-item>\n }\n @for (r of _offline.queueAPIRequests; track r; let odd = $odd) {\n <ion-item [class.oddItem]=\"odd\">\n <ion-label class=\"ion-text-wrap\">\n {{ r.description }}\n @if (r.error) {\n <p>\n <ion-text color=\"danger\">\n <b>{{ 'IDEA_UNCOMMON.OFFLINE.ERROR_CODE' | translate }}: {{ r.error }}</b>\n </ion-text>\n <br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_I' | translate }}<br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_II' | translate }}<br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_III' | translate }}\n </p>\n }\n </ion-label>\n @if (r.error) {\n <ion-button\n fill=\"clear\"\n slot=\"end\"\n color=\"danger\"\n [title]=\"'IDEA_UNCOMMON.OFFLINE.CANCEL_REQUEST' | translate\"\n (click)=\"deleteRequest(r)\"\n >\n <ion-icon slot=\"icon-only\" name=\"trash\" />\n </ion-button>\n }\n @if (r.error) {\n <ion-button\n fill=\"clear\"\n slot=\"end\"\n color=\"medium\"\n [title]=\"'IDEA_UNCOMMON.OFFLINE.DOWNLOAD_LOG' | translate\"\n (click)=\"downloadRequestLog(r)\"\n >\n <ion-icon slot=\"icon-only\" name=\"download\" />\n </ion-button>\n }\n @if (_offline.synchronizing) {\n <ion-spinner slot=\"end\" name=\"dots\" />\n }\n </ion-item>\n }\n </ion-list>\n }\n @if (_offline.getResources().length) {\n <ion-list class=\"aList\">\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.DOWNLOAD' | translate }}</h2>\n <p>{{ 'IDEA_UNCOMMON.OFFLINE.DOWNLOAD_RESOURCES_EXPLANATION' | translate }}</p>\n </ion-label>\n </ion-list-header>\n @for (r of _offline.getResources(); track r) {\n <idea-cacheable-resource [resource]=\"_offline.getCacheableResource(r)\" />\n }\n </ion-list>\n }\n</ion-content>\n", styles: [".aList{max-width:500px;margin:0 auto}.aList ion-list-header{padding-right:10px}.aList ion-list-header h3.lastSyncAt{margin-top:6px}\n"], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i1$1.IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: i1$1.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i1$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i1$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IDEACacheableResourceComponent, selector: "idea-cacheable-resource", inputs: ["resource"] }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }, { kind: "pipe", type: i3.IDEALocalizedDatePipe, name: "dateLocale" }] }); }
2178
+ }
2179
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineManagerComponent, decorators: [{
2180
+ type: Component,
2181
+ args: [{ selector: 'idea-offline-manager', template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>\n {{ 'IDEA_UNCOMMON.OFFLINE.OFFLINE_MANAGER' | translate }}\n </ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'IDEA_UNCOMMON.OFFLINE.ADVANCED_ACTIONS' | translate\" (click)=\"actions()\">\n <ion-icon slot=\"icon-only\" name=\"ellipsis-vertical\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-list class=\"aList\">\n @if (!_offline.synchronizing && _offline.requiresManualConfirmation) {\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.HUGE_SYNC_NEED_MANUAL_CONFIRMATION' | translate }}</h2>\n </ion-label>\n </ion-list-header>\n }\n @if (!_offline.synchronizing) {\n <ion-list-header>\n <ion-label class=\"ion-text-wrap\">\n <h3 class=\"lastSyncAt\">\n {{ 'IDEA_UNCOMMON.OFFLINE.LAST_SYNC' | translate }}:\n <ion-text>\n {{\n _offline.lastSyncAt\n ? (_offline.lastSyncAt | dateLocale: 'medium')\n : ('IDEA_UNCOMMON.OFFLINE.NEVER' | translate)\n }}\n </ion-text>\n </h3>\n @if (_offline.errorInLastSync) {\n <p>\n <ion-text color=\"danger\">\n <b>{{ 'IDEA_UNCOMMON.OFFLINE.ERROR_DURING_LAST_SYNC' | translate }}</b>\n </ion-text>\n </p>\n }\n </ion-label>\n <ion-button\n size=\"small\"\n fill=\"solid\"\n [color]=\"_offline.errorInLastSync ? 'danger' : 'dark'\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"sync()\"\n >\n <ion-icon slot=\"start\" name=\"sync\" /> {{ 'IDEA_UNCOMMON.OFFLINE.SYNC_NOW' | translate }}\n </ion-button>\n </ion-list-header>\n }\n </ion-list>\n @if (_offline.synchronizing) {\n <ion-list class=\"aList\">\n <ion-item>\n <ion-label class=\"ion-text-center\">\n <ion-spinner name=\"crescent\" color=\"primary\" />\n </ion-label>\n </ion-item>\n </ion-list>\n }\n @if (_offline.useQueueAPIRequests) {\n <ion-list class=\"aList\">\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.UPLOAD' | translate }}</h2>\n <p>{{ 'IDEA_UNCOMMON.OFFLINE.UPLOAD_QUEUE_REQUESTS_EXPLANATION' | translate }}</p>\n </ion-label>\n </ion-list-header>\n @if (!_offline.queueAPIRequests.length) {\n <ion-item>\n <i>{{ 'IDEA_UNCOMMON.OFFLINE.NO_ELEMENTS_TO_UPLOAD' | translate }}</i>\n <ion-icon slot=\"end\" size=\"small\" name=\"checkmark\" color=\"success\" />\n </ion-item>\n }\n @for (r of _offline.queueAPIRequests; track r; let odd = $odd) {\n <ion-item [class.oddItem]=\"odd\">\n <ion-label class=\"ion-text-wrap\">\n {{ r.description }}\n @if (r.error) {\n <p>\n <ion-text color=\"danger\">\n <b>{{ 'IDEA_UNCOMMON.OFFLINE.ERROR_CODE' | translate }}: {{ r.error }}</b>\n </ion-text>\n <br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_I' | translate }}<br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_II' | translate }}<br />\n {{ 'IDEA_UNCOMMON.OFFLINE.REQUEST_FAILED_EXPLANATION_III' | translate }}\n </p>\n }\n </ion-label>\n @if (r.error) {\n <ion-button\n fill=\"clear\"\n slot=\"end\"\n color=\"danger\"\n [title]=\"'IDEA_UNCOMMON.OFFLINE.CANCEL_REQUEST' | translate\"\n (click)=\"deleteRequest(r)\"\n >\n <ion-icon slot=\"icon-only\" name=\"trash\" />\n </ion-button>\n }\n @if (r.error) {\n <ion-button\n fill=\"clear\"\n slot=\"end\"\n color=\"medium\"\n [title]=\"'IDEA_UNCOMMON.OFFLINE.DOWNLOAD_LOG' | translate\"\n (click)=\"downloadRequestLog(r)\"\n >\n <ion-icon slot=\"icon-only\" name=\"download\" />\n </ion-button>\n }\n @if (_offline.synchronizing) {\n <ion-spinner slot=\"end\" name=\"dots\" />\n }\n </ion-item>\n }\n </ion-list>\n }\n @if (_offline.getResources().length) {\n <ion-list class=\"aList\">\n <ion-list-header>\n <ion-label>\n <h2>{{ 'IDEA_UNCOMMON.OFFLINE.DOWNLOAD' | translate }}</h2>\n <p>{{ 'IDEA_UNCOMMON.OFFLINE.DOWNLOAD_RESOURCES_EXPLANATION' | translate }}</p>\n </ion-label>\n </ion-list-header>\n @for (r of _offline.getResources(); track r) {\n <idea-cacheable-resource [resource]=\"_offline.getCacheableResource(r)\" />\n }\n </ion-list>\n }\n</ion-content>\n", styles: [".aList{max-width:500px;margin:0 auto}.aList ion-list-header{padding-right:10px}.aList ion-list-header h3.lastSyncAt{margin-top:6px}\n"] }]
2182
+ }] });
2183
+
2184
+ class IDEAOfflineIndicatorComponent {
2185
+ constructor() {
2186
+ this._alert = inject(AlertController);
2187
+ this._modal = inject(ModalController);
2188
+ this._translate = inject(IDEATranslationsService);
2189
+ this._platform = inject(Platform);
2190
+ this._offline = inject(IDEAOfflineDataService);
2191
+ /**
2192
+ * Vertical position.
2193
+ */
2194
+ this.vertical = 'bottom';
2195
+ /**
2196
+ * Horizontal position.
2197
+ */
2198
+ this.horizontal = 'start';
2199
+ /**
2200
+ * Whether it is positionated on an edge.
2201
+ */
2202
+ this.edge = false;
2203
+ }
2204
+ async showStatus() {
2205
+ if (this._offline.isOffline()) {
2206
+ const isOnline = await this._offline.check();
2207
+ if (!isOnline) {
2208
+ const alert = await this._alert.create({
2209
+ header: this._translate._('IDEA_UNCOMMON.OFFLINE.YOU_ARE_OFFLINE'),
2210
+ message: this._translate._('IDEA_UNCOMMON.OFFLINE.FEATURES_REDUCED_CONTENTS_NOT_UP_TO_DATE'),
2211
+ buttons: [this._translate._('COMMON.GOT_IT')]
2212
+ });
2213
+ alert.present();
2214
+ }
2215
+ }
2216
+ else if (this._offline.isAllowed()) {
2217
+ const modal = await this._modal.create({ component: IDEAOfflineManagerComponent });
2218
+ modal.present();
2219
+ }
2220
+ }
2221
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2222
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAOfflineIndicatorComponent, selector: "idea-offline-indicator", inputs: { vertical: "vertical", horizontal: "horizontal", edge: "edge" }, ngImport: i0, template: "@if (\n _platform.is('mobile') && (_offline.isOffline() || _offline.synchronizing || _offline.requiresManualConfirmation)\n) {\n <ion-fab [vertical]=\"vertical\" [horizontal]=\"horizontal\" [edge]=\"edge\">\n <ion-fab-button color=\"dark\" size=\"small\" (click)=\"showStatus()\">\n <!-- OFFLINE -->\n @if (_offline.isOffline()) {\n <ion-icon name=\"airplane\" />\n }\n <!-- SYNCHRONISING -->\n @if (_offline.isOnline() && _offline.synchronizing) {\n <ion-icon name=\"sync\" />\n }\n <!-- NEED MANAL SYNCHRONISATION -->\n @if (_offline.isOnline() && _offline.requiresManualConfirmation) {\n <ion-icon name=\"pause\" />\n }\n </ion-fab-button>\n </ion-fab>\n}\n", styles: [""], dependencies: [{ kind: "component", type: i1$1.IonFab, selector: "ion-fab", inputs: ["activated", "edge", "horizontal", "vertical"] }, { kind: "component", type: i1$1.IonFabButton, selector: "ion-fab-button", inputs: ["activated", "closeIcon", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
2223
+ }
2224
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineIndicatorComponent, decorators: [{
2225
+ type: Component,
2226
+ args: [{ selector: 'idea-offline-indicator', template: "@if (\n _platform.is('mobile') && (_offline.isOffline() || _offline.synchronizing || _offline.requiresManualConfirmation)\n) {\n <ion-fab [vertical]=\"vertical\" [horizontal]=\"horizontal\" [edge]=\"edge\">\n <ion-fab-button color=\"dark\" size=\"small\" (click)=\"showStatus()\">\n <!-- OFFLINE -->\n @if (_offline.isOffline()) {\n <ion-icon name=\"airplane\" />\n }\n <!-- SYNCHRONISING -->\n @if (_offline.isOnline() && _offline.synchronizing) {\n <ion-icon name=\"sync\" />\n }\n <!-- NEED MANAL SYNCHRONISATION -->\n @if (_offline.isOnline() && _offline.requiresManualConfirmation) {\n <ion-icon name=\"pause\" />\n }\n </ion-fab-button>\n </ion-fab>\n}\n" }]
2227
+ }], propDecorators: { vertical: [{
2228
+ type: Input
2229
+ }], horizontal: [{
2230
+ type: Input
2231
+ }], edge: [{
2232
+ type: Input
2233
+ }] } });
2234
+
2235
+ class IDEAOfflineModule {
2236
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
2237
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineModule, declarations: [IDEAOfflineManagerComponent, IDEAOfflineIndicatorComponent, IDEACacheableResourceComponent], imports: [CommonModule, IonicModule, IDEATranslationsModule], exports: [IDEAOfflineManagerComponent, IDEAOfflineIndicatorComponent, IDEACacheableResourceComponent] }); }
2238
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineModule, imports: [CommonModule, IonicModule, IDEATranslationsModule] }); }
2239
+ }
2240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAOfflineModule, decorators: [{
2241
+ type: NgModule,
2242
+ args: [{
2243
+ imports: [CommonModule, IonicModule, IDEATranslationsModule],
2244
+ declarations: [IDEAOfflineManagerComponent, IDEAOfflineIndicatorComponent, IDEACacheableResourceComponent],
2245
+ exports: [IDEAOfflineManagerComponent, IDEAOfflineIndicatorComponent, IDEACacheableResourceComponent]
2246
+ }]
2247
+ }] });
2248
+
2249
+ class IDEASentimentComponent {
2250
+ constructor() {
2251
+ this._env = inject(IDEAEnvironment);
2252
+ this._API = inject(IDEAAWSAPIService);
2253
+ this._translate = inject(IDEATranslationsService);
2254
+ this._offline = inject(IDEAOfflineService);
2255
+ /**
2256
+ * Triggers when the sentiment change.
2257
+ */
2258
+ this.change = new EventEmitter();
2259
+ }
2260
+ ngOnChanges(changes) {
2261
+ if (changes.text.previousValue !== changes.text.currentValue)
2262
+ this.detectSentiment(changes.text.currentValue);
2263
+ }
2264
+ async detectSentiment(text) {
2265
+ if (!text || this._offline.isOffline())
2266
+ this.sentiment = null;
2267
+ else {
2268
+ try {
2269
+ const { sentiment } = await this._API.postResource('sentiment', {
2270
+ idea: true,
2271
+ body: { project: this._env.idea.project, language: this._translate.getCurrentLang(), text: this.text }
2272
+ });
2273
+ this.sentiment = sentiment;
2274
+ this.change.emit(this.sentiment);
2275
+ }
2276
+ catch (error) {
2277
+ this.sentiment = null;
2278
+ }
2279
+ }
2280
+ }
2281
+ getColorBySentiment(sentiment) {
2282
+ sentiment = sentiment || this.sentiment;
2283
+ switch (sentiment) {
2284
+ case Sentiment.POSITIVE:
2285
+ return 'success';
2286
+ case Sentiment.NEGATIVE:
2287
+ return 'danger';
2288
+ case Sentiment.MIXED:
2289
+ return 'warning';
2290
+ case Sentiment.NEUTRAL:
2291
+ return 'medium';
2292
+ default:
2293
+ return 'light';
2294
+ }
2295
+ }
2296
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2297
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEASentimentComponent, selector: "idea-sentiment", inputs: { text: "text", lines: "lines", color: "color" }, outputs: { change: "change" }, usesOnChanges: true, ngImport: i0, template: "@if (text && _offline.isOnline()) {\n <ion-item [lines]=\"lines\" [color]=\"color\">\n @if (!sentiment) {\n <ion-spinner slot=\"end\" size=\"small\" />\n }\n @if (sentiment) {\n <ion-badge\n slot=\"end\"\n size=\"small\"\n [color]=\"getColorBySentiment()\"\n [title]=\"'IDEA_UNCOMMON.SENTIMENT.RESULT_HINT' | translate: { sentiment: sentiment }\"\n >\n {{ 'IDEA_UNCOMMON.SENTIMENT.RESULT.' + sentiment | translate }}\n </ion-badge>\n }\n </ion-item>\n}\n", styles: [""], dependencies: [{ kind: "component", type: i1$1.IonBadge, selector: "ion-badge", inputs: ["color", "mode"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
2298
+ }
2299
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentComponent, decorators: [{
2300
+ type: Component,
2301
+ args: [{ selector: 'idea-sentiment', template: "@if (text && _offline.isOnline()) {\n <ion-item [lines]=\"lines\" [color]=\"color\">\n @if (!sentiment) {\n <ion-spinner slot=\"end\" size=\"small\" />\n }\n @if (sentiment) {\n <ion-badge\n slot=\"end\"\n size=\"small\"\n [color]=\"getColorBySentiment()\"\n [title]=\"'IDEA_UNCOMMON.SENTIMENT.RESULT_HINT' | translate: { sentiment: sentiment }\"\n >\n {{ 'IDEA_UNCOMMON.SENTIMENT.RESULT.' + sentiment | translate }}\n </ion-badge>\n }\n </ion-item>\n}\n" }]
2302
+ }], propDecorators: { text: [{
2303
+ type: Input
2304
+ }], lines: [{
2305
+ type: Input
2306
+ }], color: [{
2307
+ type: Input
2308
+ }], change: [{
2309
+ type: Output
2310
+ }] } });
2311
+
2312
+ class IDEASentimentModule {
2313
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
2314
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentModule, declarations: [IDEASentimentComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEASentimentComponent] }); }
2315
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
2316
+ }
2317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEASentimentModule, decorators: [{
2318
+ type: NgModule,
2319
+ args: [{
2320
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
2321
+ declarations: [IDEASentimentComponent],
2322
+ exports: [IDEASentimentComponent]
2323
+ }]
2324
+ }] });
2325
+
2326
+ class IDEAFromTimeToTimeComponent {
2327
+ constructor() {
2328
+ this._modal = inject(ModalController);
2329
+ this._translate = inject(IDEATranslationsService);
2330
+ /**
2331
+ * Whether we should start picking the time displaying the afternoon (PM) or the morning (AM, default).
2332
+ */
2333
+ this.period = Periods.AM;
2334
+ this.Segments = Segments;
2335
+ this.Periods = Periods;
2336
+ }
2337
+ ngOnInit() {
2338
+ this.segment = Segments.FROM;
2339
+ this.minutes = 30;
2340
+ this.timeIntervalWC = new TimeInterval(this.timeInterval);
2341
+ }
2342
+ setInterval(hours, minutes) {
2343
+ // adjust the hour based on the start of the interval and the settings of am/pm
2344
+ hours = this.contextualizeHour(hours, minutes);
2345
+ // convert the hours and minutes in milliseconds
2346
+ const timeMs = this.timeToMs(hours, minutes);
2347
+ // set the correct part of the interval, based on the current segment
2348
+ if (this.segment === Segments.FROM) {
2349
+ // set the start of the interval (from) and change the segment so that the end (to) will be picked
2350
+ this.timeIntervalWC.from = timeMs;
2351
+ this.segment = Segments.TO;
2352
+ }
2353
+ else {
2354
+ // can't proceed if from and to are equal
2355
+ if (timeMs === this.timeIntervalWC.from)
2356
+ return;
2357
+ // set the end of the interval (to)
2358
+ this.timeIntervalWC.to = timeMs;
2359
+ // save the changes and notify the parent component
2360
+ this.save();
2361
+ }
2362
+ }
2363
+ timeToMs(hours, minutes) {
2364
+ return (hours * 60 * 60 + (minutes || 0) * 60) * 1000;
2365
+ }
2366
+ getClockButtonContent(hours, minutes) {
2367
+ // adjust the hour based on the start of the interval and the settings of am/pm
2368
+ hours = this.contextualizeHour(hours, minutes);
2369
+ // convert the calculated time into a readable string; we need to format it to distinguish 0-24 vs 0-12 am/pm
2370
+ const refDate = new Date();
2371
+ refDate.setHours(hours, minutes || 0, 0);
2372
+ const str = this._translate.formatDate(refDate, 'shortTime');
2373
+ // remove the extra part (e.g. am/pm), based on the type of button we have to show
2374
+ const strArr = str.split(':');
2375
+ return String(Number(strArr[0])).concat(minutes ? ':'.concat(strArr[1].slice(0, 2)) : '');
2376
+ }
2377
+ contextualizeHour(hours, minutes) {
2378
+ // adjust the content to the afternoon hours, if needed
2379
+ if (this.period === Periods.PM)
2380
+ hours += 12;
2381
+ // if we already selected the start of the interval, check whether we need to adjust the time: the center of the
2382
+ // clock will be moved to the selected hour (from), to have a reasonable set of choices for the end (to)
2383
+ if (this.segment === Segments.TO && this.timeToMs(hours, minutes) < this.timeIntervalWC.from)
2384
+ hours += this.period === Periods.PM ? -12 : 12;
2385
+ // set midnight to 00
2386
+ if (hours === 24)
2387
+ hours = 0;
2388
+ return hours;
2389
+ }
2390
+ equalsFrom(hours, minutes) {
2391
+ // if the start of the interval hasn't been selected yet, skip it
2392
+ if (this.segment === Segments.FROM)
2393
+ return false;
2394
+ // adjust the hour based on the start of the interval and the settings of am/pm
2395
+ hours = this.contextualizeHour(hours, minutes);
2396
+ // compare the two strings based on the timestamps in ms
2397
+ return this.msToTimeString(this.timeIntervalWC.from) === this.msToTimeString(this.timeToMs(hours, minutes));
2398
+ }
2399
+ isAllowed(hours, minutes) {
2400
+ // adjust the hour based on the start of the interval and the settings of am/pm
2401
+ hours = this.contextualizeHour(hours, minutes);
2402
+ // check whether the selected time is inside the given boundaries
2403
+ return ((!this.notEarlierThan ||
2404
+ this.timeToMs(hours, minutes) >= this.notEarlierThan ||
2405
+ this.timeToMs(hours, minutes) === 0) &&
2406
+ (!this.notLaterThan || this.timeToMs(hours, minutes) <= this.notLaterThan));
2407
+ }
2408
+ isHourOfOppositeMeridian(hours, minutes) {
2409
+ // if the start of the interval hasn't been selected yet, skip it
2410
+ if (this.segment === Segments.FROM)
2411
+ return false;
2412
+ // adjust the hour based on the start of the interval and the settings of am/pm
2413
+ hours = this.contextualizeHour(hours, minutes);
2414
+ // calculate the ms of the selected time and of "noon" time (to determ the meridian)
2415
+ const timeMs = this.timeToMs(hours, minutes);
2416
+ const noonMs = this.timeToMs(12);
2417
+ // return true in case the hour doesn't belong to the current meridian
2418
+ return (this.period === Periods.AM && timeMs > noonMs) || (this.period === Periods.PM && timeMs < noonMs);
2419
+ }
2420
+ msToTimeString(time) {
2421
+ if (!time)
2422
+ return '';
2423
+ // note: the time must be always considered without any timezone (UTC)
2424
+ const refDate = new Date();
2425
+ refDate.setTime(time);
2426
+ const dateOpts = { timeZone: 'UTC', hour: '2-digit', minute: '2-digit' };
2427
+ return refDate.toLocaleTimeString(this._translate.getCurrentLang(), dateOpts);
2428
+ }
2429
+ save() {
2430
+ this.timeInterval.load(this.timeIntervalWC);
2431
+ this._modal.dismiss(true);
2432
+ }
2433
+ reset() {
2434
+ this.timeInterval.reset();
2435
+ this._modal.dismiss(false);
2436
+ }
2437
+ close() {
2438
+ this._modal.dismiss();
2439
+ }
2440
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAFromTimeToTimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2441
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEAFromTimeToTimeComponent, selector: "idea-from-time-to-time", inputs: { timeInterval: "timeInterval", period: "period", notEarlierThan: "notEarlierThan", notLaterThan: "notLaterThan", title: "title" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'COMMON.CLOSE' | translate\" (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>{{ title }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'COMMON.RESET' | translate\" (click)=\"reset()\">\n <ion-icon name=\"arrow-undo\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-grid class=\"clockGrid\">\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"5\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(12) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(12)\"\n [disabled]=\"!isAllowed(12)\"\n (click)=\"setInterval(12)\"\n >\n {{ getClockButtonContent(12) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(12, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(12, minutes)\"\n [disabled]=\"!isAllowed(12, minutes)\"\n (click)=\"setInterval(12, minutes)\"\n >\n {{ getClockButtonContent(12, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"3\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(11, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(11, minutes)\"\n [disabled]=\"!isAllowed(11, minutes)\"\n (click)=\"setInterval(11, minutes)\"\n >\n {{ getClockButtonContent(11, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(11) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(11)\"\n [disabled]=\"!isAllowed(11)\"\n (click)=\"setInterval(11)\"\n >\n {{ getClockButtonContent(11) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(1) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(1)\"\n [disabled]=\"!isAllowed(1)\"\n (click)=\"setInterval(1)\"\n >\n {{ getClockButtonContent(1) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(1, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(1, minutes)\"\n [disabled]=\"!isAllowed(1, minutes)\"\n (click)=\"setInterval(1, minutes)\"\n >\n {{ getClockButtonContent(1, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(10, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(10, minutes)\"\n [disabled]=\"!isAllowed(10, minutes)\"\n (click)=\"setInterval(10, minutes)\"\n >\n {{ getClockButtonContent(10, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(10) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(10)\"\n [disabled]=\"!isAllowed(10)\"\n (click)=\"setInterval(10)\"\n >\n {{ getClockButtonContent(10) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(2) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(2)\"\n [disabled]=\"!isAllowed(2)\"\n (click)=\"setInterval(2)\"\n >\n {{ getClockButtonContent(2) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(2, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(2, minutes)\"\n [disabled]=\"!isAllowed(2, minutes)\"\n (click)=\"setInterval(2, minutes)\"\n >\n {{ getClockButtonContent(2, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(9, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(9, minutes)\"\n [disabled]=\"!isAllowed(9, minutes)\"\n (click)=\"setInterval(9, minutes)\"\n >\n {{ getClockButtonContent(9, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(9) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(9)\"\n [disabled]=\"!isAllowed(9)\"\n (click)=\"setInterval(9)\"\n >\n {{ getClockButtonContent(9) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center displayText\" [size]=\"4\">\n <ion-text [class.fromSelected]=\"segment === Segments.TO\">\n {{ 'IDEA_UNCOMMON.FTTT.FROM' | translate }}\n @if (segment === Segments.TO) {\n <span>{{ msToTimeString(_timeInterval.from) }}</span>\n }\n </ion-text>\n @if (segment === Segments.TO) {\n <ion-text>{{ 'IDEA_UNCOMMON.FTTT.TO' | translate }}</ion-text>\n }\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(3) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(3)\"\n [disabled]=\"!isAllowed(3)\"\n (click)=\"setInterval(3)\"\n >\n {{ getClockButtonContent(3) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(3, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(3, minutes)\"\n [disabled]=\"!isAllowed(3, minutes)\"\n (click)=\"setInterval(3, minutes)\"\n >\n {{ getClockButtonContent(3, minutes) }}\n </ion-button>\n </ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(8, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(8, minutes)\"\n [disabled]=\"!isAllowed(8, minutes)\"\n (click)=\"setInterval(8, minutes)\"\n >\n {{ getClockButtonContent(8, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(8) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(8)\"\n [disabled]=\"!isAllowed(8)\"\n (click)=\"setInterval(8)\"\n >\n {{ getClockButtonContent(8) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(4) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(4)\"\n [disabled]=\"!isAllowed(4)\"\n (click)=\"setInterval(4)\"\n >\n {{ getClockButtonContent(4) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(4, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(4, minutes)\"\n [disabled]=\"!isAllowed(4, minutes)\"\n (click)=\"setInterval(4, minutes)\"\n >\n {{ getClockButtonContent(4, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(7, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(7, minutes)\"\n [disabled]=\"!isAllowed(7, minutes)\"\n (click)=\"setInterval(7, minutes)\"\n >\n {{ getClockButtonContent(7, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(7) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(7)\"\n [disabled]=\"!isAllowed(7)\"\n (click)=\"setInterval(7)\"\n >\n {{ getClockButtonContent(7) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(5) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(5)\"\n [disabled]=\"!isAllowed(5)\"\n (click)=\"setInterval(5)\"\n >\n {{ getClockButtonContent(5) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(5, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(5, minutes)\"\n [disabled]=\"!isAllowed(5, minutes)\"\n (click)=\"setInterval(5, minutes)\"\n >\n {{ getClockButtonContent(5, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"3\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(6, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(6, minutes)\"\n [disabled]=\"!isAllowed(6, minutes)\"\n (click)=\"setInterval(6, minutes)\"\n >\n {{ getClockButtonContent(6, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(6) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(6)\"\n [disabled]=\"!isAllowed(6)\"\n (click)=\"setInterval(6)\"\n >\n {{ getClockButtonContent(6) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"5\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right\" [size]=\"3\" [sizeSm]=\"7\">\n <ion-text class=\"prefLabel\">{{ 'IDEA_UNCOMMON.FTTT.PERIOD' | translate }}:</ion-text>\n </ion-col>\n <ion-col [size]=\"9\" [sizeSm]=\"5\">\n <ion-segment [value]=\"period\" (ionChange)=\"period = $event?.detail?.value || period\">\n <ion-segment-button [value]=\"Periods.AM\">\n <ion-label>{{ 'IDEA_UNCOMMON.FTTT.AM' | translate }}</ion-label>\n </ion-segment-button>\n <ion-segment-button [value]=\"Periods.PM\">\n <ion-label>{{ 'IDEA_UNCOMMON.FTTT.PM' | translate }}</ion-label>\n </ion-segment-button>\n </ion-segment>\n </ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right\" [size]=\"3\" [sizeSm]=\"7\">\n <ion-text class=\"prefLabel\">{{ 'IDEA_UNCOMMON.FTTT.MINUTES_PREFERENCE' | translate }}:</ion-text>\n </ion-col>\n <ion-col [size]=\"9\" [sizeSm]=\"5\">\n <ion-segment [value]=\"minutes\" (ionChange)=\"minutes = $event?.detail?.value || minutes\">\n <ion-segment-button value=\"15\">\n <ion-label>15</ion-label>\n </ion-segment-button>\n <ion-segment-button value=\"30\">\n <ion-label>30</ion-label>\n </ion-segment-button>\n <ion-segment-button value=\"45\">\n <ion-label>45</ion-label>\n </ion-segment-button>\n </ion-segment>\n </ion-col>\n </ion-row>\n </ion-grid>\n</ion-content>\n", styles: ["ion-grid.clockGrid ion-button[size=large]{width:50px;height:50px;font-size:1.1em;font-weight:700}ion-grid.clockGrid ion-button[size=small]{font-size:.75em}ion-grid.clockGrid ion-button.oppositeMeridian{--border-color: var(--ion-color-medium);--border-style: dashed;--border-width: 1px}ion-grid.clockGrid .displayText ion-text{font-size:1.3em;font-weight:700}ion-grid.clockGrid .displayText ion-text.fromSelected{display:block;padding-bottom:3px;font-size:.9em}ion-grid.clockGrid ion-segment ion-segment-button{min-width:50px;--color-checked: var(--ion-color-dark);--border-color: var(--ion-color-dark)}ion-grid.clockGrid .prefLabel{font-size:.6em;font-weight:700;text-transform:uppercase}\n"], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i1$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i1$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonRow, selector: "ion-row" }, { kind: "component", type: i1$1.IonSegment, selector: "ion-segment", inputs: ["color", "disabled", "mode", "scrollable", "selectOnFocus", "swipeGesture", "value"] }, { kind: "component", type: i1$1.IonSegmentButton, selector: "ion-segment-button", inputs: ["disabled", "layout", "mode", "type", "value"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i1$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i1$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.SelectValueAccessor, selector: "ion-select, ion-radio-group, ion-segment, ion-datetime" }, { kind: "pipe", type: i3.IDEATranslatePipe, name: "translate" }] }); }
2442
+ }
2443
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAFromTimeToTimeComponent, decorators: [{
2444
+ type: Component,
2445
+ args: [{ selector: 'idea-from-time-to-time', template: "<ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'COMMON.CLOSE' | translate\" (click)=\"close()\">\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>{{ title }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button [title]=\"'COMMON.RESET' | translate\" (click)=\"reset()\">\n <ion-icon name=\"arrow-undo\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n<ion-content>\n <ion-grid class=\"clockGrid\">\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"5\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(12) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(12)\"\n [disabled]=\"!isAllowed(12)\"\n (click)=\"setInterval(12)\"\n >\n {{ getClockButtonContent(12) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(12, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(12, minutes)\"\n [disabled]=\"!isAllowed(12, minutes)\"\n (click)=\"setInterval(12, minutes)\"\n >\n {{ getClockButtonContent(12, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"3\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(11, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(11, minutes)\"\n [disabled]=\"!isAllowed(11, minutes)\"\n (click)=\"setInterval(11, minutes)\"\n >\n {{ getClockButtonContent(11, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(11) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(11)\"\n [disabled]=\"!isAllowed(11)\"\n (click)=\"setInterval(11)\"\n >\n {{ getClockButtonContent(11) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(1) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(1)\"\n [disabled]=\"!isAllowed(1)\"\n (click)=\"setInterval(1)\"\n >\n {{ getClockButtonContent(1) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(1, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(1, minutes)\"\n [disabled]=\"!isAllowed(1, minutes)\"\n (click)=\"setInterval(1, minutes)\"\n >\n {{ getClockButtonContent(1, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(10, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(10, minutes)\"\n [disabled]=\"!isAllowed(10, minutes)\"\n (click)=\"setInterval(10, minutes)\"\n >\n {{ getClockButtonContent(10, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(10) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(10)\"\n [disabled]=\"!isAllowed(10)\"\n (click)=\"setInterval(10)\"\n >\n {{ getClockButtonContent(10) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(2) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(2)\"\n [disabled]=\"!isAllowed(2)\"\n (click)=\"setInterval(2)\"\n >\n {{ getClockButtonContent(2) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(2, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(2, minutes)\"\n [disabled]=\"!isAllowed(2, minutes)\"\n (click)=\"setInterval(2, minutes)\"\n >\n {{ getClockButtonContent(2, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(9, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(9, minutes)\"\n [disabled]=\"!isAllowed(9, minutes)\"\n (click)=\"setInterval(9, minutes)\"\n >\n {{ getClockButtonContent(9, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(9) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(9)\"\n [disabled]=\"!isAllowed(9)\"\n (click)=\"setInterval(9)\"\n >\n {{ getClockButtonContent(9) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center displayText\" [size]=\"4\">\n <ion-text [class.fromSelected]=\"segment === Segments.TO\">\n {{ 'IDEA_UNCOMMON.FTTT.FROM' | translate }}\n @if (segment === Segments.TO) {\n <span>{{ msToTimeString(_timeInterval.from) }}</span>\n }\n </ion-text>\n @if (segment === Segments.TO) {\n <ion-text>{{ 'IDEA_UNCOMMON.FTTT.TO' | translate }}</ion-text>\n }\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(3) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(3)\"\n [disabled]=\"!isAllowed(3)\"\n (click)=\"setInterval(3)\"\n >\n {{ getClockButtonContent(3) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(3, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(3, minutes)\"\n [disabled]=\"!isAllowed(3, minutes)\"\n (click)=\"setInterval(3, minutes)\"\n >\n {{ getClockButtonContent(3, minutes) }}\n </ion-button>\n </ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(8, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(8, minutes)\"\n [disabled]=\"!isAllowed(8, minutes)\"\n (click)=\"setInterval(8, minutes)\"\n >\n {{ getClockButtonContent(8, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(8) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(8)\"\n [disabled]=\"!isAllowed(8)\"\n (click)=\"setInterval(8)\"\n >\n {{ getClockButtonContent(8) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(4) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(4)\"\n [disabled]=\"!isAllowed(4)\"\n (click)=\"setInterval(4)\"\n >\n {{ getClockButtonContent(4) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(4, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(4, minutes)\"\n [disabled]=\"!isAllowed(4, minutes)\"\n (click)=\"setInterval(4, minutes)\"\n >\n {{ getClockButtonContent(4, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"1\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(7, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(7, minutes)\"\n [disabled]=\"!isAllowed(7, minutes)\"\n (click)=\"setInterval(7, minutes)\"\n >\n {{ getClockButtonContent(7, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-left\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(7) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(7)\"\n [disabled]=\"!isAllowed(7)\"\n (click)=\"setInterval(7)\"\n >\n {{ getClockButtonContent(7) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(5) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(5)\"\n [disabled]=\"!isAllowed(5)\"\n (click)=\"setInterval(5)\"\n >\n {{ getClockButtonContent(5) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(5, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(5, minutes)\"\n [disabled]=\"!isAllowed(5, minutes)\"\n (click)=\"setInterval(5, minutes)\"\n >\n {{ getClockButtonContent(5, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"2\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col [size]=\"3\">&nbsp;</ion-col>\n <ion-col class=\"ion-text-right\" [size]=\"2\">\n <ion-button\n size=\"small\"\n [color]=\"equalsFrom(6, minutes) ? 'primary' : 'light'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(6, minutes)\"\n [disabled]=\"!isAllowed(6, minutes)\"\n (click)=\"setInterval(6, minutes)\"\n >\n {{ getClockButtonContent(6, minutes) }}\n </ion-button>\n </ion-col>\n <ion-col class=\"ion-text-center\" [size]=\"2\">\n <ion-button\n size=\"large\"\n shape=\"round\"\n [color]=\"equalsFrom(6) ? 'primary' : 'dark'\"\n [class.oppositeMeridian]=\"isHourOfOppositeMeridian(6)\"\n [disabled]=\"!isAllowed(6)\"\n (click)=\"setInterval(6)\"\n >\n {{ getClockButtonContent(6) }}\n </ion-button>\n </ion-col>\n <ion-col [size]=\"5\">&nbsp;</ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right\" [size]=\"3\" [sizeSm]=\"7\">\n <ion-text class=\"prefLabel\">{{ 'IDEA_UNCOMMON.FTTT.PERIOD' | translate }}:</ion-text>\n </ion-col>\n <ion-col [size]=\"9\" [sizeSm]=\"5\">\n <ion-segment [value]=\"period\" (ionChange)=\"period = $event?.detail?.value || period\">\n <ion-segment-button [value]=\"Periods.AM\">\n <ion-label>{{ 'IDEA_UNCOMMON.FTTT.AM' | translate }}</ion-label>\n </ion-segment-button>\n <ion-segment-button [value]=\"Periods.PM\">\n <ion-label>{{ 'IDEA_UNCOMMON.FTTT.PM' | translate }}</ion-label>\n </ion-segment-button>\n </ion-segment>\n </ion-col>\n </ion-row>\n <ion-row class=\"ion-align-items-center\">\n <ion-col class=\"ion-text-right\" [size]=\"3\" [sizeSm]=\"7\">\n <ion-text class=\"prefLabel\">{{ 'IDEA_UNCOMMON.FTTT.MINUTES_PREFERENCE' | translate }}:</ion-text>\n </ion-col>\n <ion-col [size]=\"9\" [sizeSm]=\"5\">\n <ion-segment [value]=\"minutes\" (ionChange)=\"minutes = $event?.detail?.value || minutes\">\n <ion-segment-button value=\"15\">\n <ion-label>15</ion-label>\n </ion-segment-button>\n <ion-segment-button value=\"30\">\n <ion-label>30</ion-label>\n </ion-segment-button>\n <ion-segment-button value=\"45\">\n <ion-label>45</ion-label>\n </ion-segment-button>\n </ion-segment>\n </ion-col>\n </ion-row>\n </ion-grid>\n</ion-content>\n", styles: ["ion-grid.clockGrid ion-button[size=large]{width:50px;height:50px;font-size:1.1em;font-weight:700}ion-grid.clockGrid ion-button[size=small]{font-size:.75em}ion-grid.clockGrid ion-button.oppositeMeridian{--border-color: var(--ion-color-medium);--border-style: dashed;--border-width: 1px}ion-grid.clockGrid .displayText ion-text{font-size:1.3em;font-weight:700}ion-grid.clockGrid .displayText ion-text.fromSelected{display:block;padding-bottom:3px;font-size:.9em}ion-grid.clockGrid ion-segment ion-segment-button{min-width:50px;--color-checked: var(--ion-color-dark);--border-color: var(--ion-color-dark)}ion-grid.clockGrid .prefLabel{font-size:.6em;font-weight:700;text-transform:uppercase}\n"] }]
2446
+ }], propDecorators: { timeInterval: [{
2447
+ type: Input
2448
+ }], period: [{
2449
+ type: Input
2450
+ }], notEarlierThan: [{
2451
+ type: Input
2452
+ }], notLaterThan: [{
2453
+ type: Input
2454
+ }], title: [{
2455
+ type: Input
2456
+ }] } });
2457
+ /**
2458
+ * The two stages of the component, to pick the start and the end of the interval.
2459
+ */
2460
+ var Segments;
2461
+ (function (Segments) {
2462
+ Segments["FROM"] = "FROM";
2463
+ Segments["TO"] = "TO";
2464
+ })(Segments || (Segments = {}));
2465
+ /**
2466
+ * The two possible periods of the component, to interpret the clock the right way.
2467
+ */
2468
+ var Periods;
2469
+ (function (Periods) {
2470
+ Periods["AM"] = "AM";
2471
+ Periods["PM"] = "PM";
2472
+ })(Periods || (Periods = {}));
2473
+
2474
+ class IDEATimeIntervalComponent {
2475
+ constructor() {
2476
+ this._modal = inject(ModalController);
2477
+ this._translate = inject(IDEATranslationsService);
2478
+ /**
2479
+ * Whether we should start picking the time displaying the afternoon (PM) or the morning (AM, default).
2480
+ */
2481
+ this.period = Periods.AM;
2482
+ /**
2483
+ * On select event.
2484
+ */
2485
+ this.select = new EventEmitter();
2486
+ /**
2487
+ * Icon select.
2488
+ */
2489
+ this.iconSelect = new EventEmitter();
2490
+ /**
2491
+ * On select (with the field disabled) event.
2492
+ */
2493
+ this.selectWhenDisabled = new EventEmitter();
2494
+ this.isOpening = false;
2495
+ }
2496
+ ngOnInit() {
2497
+ // when the language changes, set the locale
2498
+ this.langChangeSubscription = this._translate.onLangChange.subscribe(() => {
2499
+ this.valueToDisplay = this.getValueToDisplay(this.timeInterval);
2500
+ });
2501
+ }
2502
+ ngOnDestroy() {
2503
+ if (this.langChangeSubscription)
2504
+ this.langChangeSubscription.unsubscribe();
2505
+ }
2506
+ ngOnChanges(changes) {
2507
+ if (changes.timeInterval)
2508
+ this.valueToDisplay = this.getValueToDisplay(changes.timeInterval.currentValue);
2509
+ }
2510
+ getValueToDisplay(timeInterval) {
2511
+ if (!timeInterval || !timeInterval.isSet())
2512
+ return '';
2513
+ // note: the time must be always considered without any timezone (UTC)
2514
+ const dateOpts = { timeZone: 'UTC', hour: '2-digit', minute: '2-digit' };
2515
+ return ''.concat(this._translate._('IDEA_UNCOMMON.FTTT.FROM'), ' ', new Date(timeInterval.from).toLocaleTimeString(this._translate.getCurrentLang(), dateOpts), ' ', this._translate._('IDEA_UNCOMMON.FTTT.TO').toLowerCase(), ' ', new Date(timeInterval.to).toLocaleTimeString(this._translate.getCurrentLang(), dateOpts));
2516
+ }
2517
+ async pickTimeInterval() {
2518
+ if (this.isOpening)
2519
+ return;
2520
+ this.isOpening = true;
2521
+ const modal = await this._modal.create({
2522
+ component: IDEAFromTimeToTimeComponent,
2523
+ componentProps: {
2524
+ timeInterval: this.timeInterval,
2525
+ period: this.period,
2526
+ notEarlierThan: this.notEarlierThan,
2527
+ notLaterThan: this.notLaterThan,
2528
+ title: this.label
2529
+ }
2530
+ });
2531
+ modal.onDidDismiss().then((res) => {
2532
+ // if the content changed, update the internal values and notify the parent component
2533
+ if (res.data === true || res.data === false) {
2534
+ this.valueToDisplay = this.getValueToDisplay(this.timeInterval);
2535
+ this.select.emit();
2536
+ }
2537
+ });
2538
+ modal.present();
2539
+ this.isOpening = false;
2540
+ }
2541
+ doSelectWhenDisabled() {
2542
+ if (this.disabled)
2543
+ this.selectWhenDisabled.emit();
2544
+ }
2545
+ doIconSelect(event) {
2546
+ if (event)
2547
+ event.stopPropagation();
2548
+ this.iconSelect.emit(event);
2549
+ }
2550
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2551
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.1.1", type: IDEATimeIntervalComponent, selector: "idea-time-interval", inputs: { timeInterval: "timeInterval", period: "period", notEarlierThan: "notEarlierThan", notLaterThan: "notLaterThan", label: "label", icon: "icon", iconColor: "iconColor", placeholder: "placeholder", disabled: "disabled", tappableWhenDisabled: "tappableWhenDisabled", obligatory: "obligatory", lines: "lines", color: "color" }, outputs: { select: "select", iconSelect: "iconSelect", selectWhenDisabled: "selectWhenDisabled" }, usesOnChanges: true, ngImport: i0, template: "<ion-item\n class=\"timeIntervalItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [button]=\"!disabled\"\n [disabled]=\"isOpening\"\n [title]=\"placeholder || valueToDisplay || ''\"\n [class.withLabel]=\"label\"\n (click)=\"disabled ? doSelectWhenDisabled() : pickTimeInterval()\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" [class.marginTop]=\"label\" (click)=\"doIconSelect($event)\">\n <ion-icon [name]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label position=\"stacked\" [class.selectable]=\"!disabled || tappableWhenDisabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"description\" [class.selectable]=\"!disabled || tappableWhenDisabled\">\n @if (!valueToDisplay && !disabled) {\n <ion-text class=\"placeholder\" [class.selectable]=\"!disabled\">\n {{ placeholder }}\n </ion-text>\n }\n {{ valueToDisplay }}\n </ion-label>\n @if (!disabled) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".timeIntervalItem{min-height:48px;height:auto}.timeIntervalItem .description{margin:10px 0;height:20px;line-height:20px;width:100%}.timeIntervalItem .placeholder{color:var(--ion-color-medium)}.timeIntervalItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.timeIntervalItem.withLabel{min-height:58px;height:auto}.timeIntervalItem.withLabel .selectIcon{padding-top:25px}.timeIntervalItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}\n"], dependencies: [{ kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i1$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }] }); }
2552
+ }
2553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalComponent, decorators: [{
2554
+ type: Component,
2555
+ args: [{ selector: 'idea-time-interval', template: "<ion-item\n class=\"timeIntervalItem\"\n [color]=\"color\"\n [lines]=\"lines\"\n [button]=\"!disabled\"\n [disabled]=\"isOpening\"\n [title]=\"placeholder || valueToDisplay || ''\"\n [class.withLabel]=\"label\"\n (click)=\"disabled ? doSelectWhenDisabled() : pickTimeInterval()\"\n>\n @if (icon) {\n <ion-button fill=\"clear\" slot=\"start\" [color]=\"iconColor\" [class.marginTop]=\"label\" (click)=\"doIconSelect($event)\">\n <ion-icon [name]=\"icon\" slot=\"icon-only\" />\n </ion-button>\n }\n @if (label) {\n <ion-label position=\"stacked\" [class.selectable]=\"!disabled || tappableWhenDisabled\">\n {{ label }}\n @if (obligatory && !disabled) {\n <ion-text class=\"obligatoryDot\" />\n }\n </ion-label>\n }\n <ion-label class=\"description\" [class.selectable]=\"!disabled || tappableWhenDisabled\">\n @if (!valueToDisplay && !disabled) {\n <ion-text class=\"placeholder\" [class.selectable]=\"!disabled\">\n {{ placeholder }}\n </ion-text>\n }\n {{ valueToDisplay }}\n </ion-label>\n @if (!disabled) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" [class.selectable]=\"!disabled\" />\n }\n</ion-item>\n", styles: [".timeIntervalItem{min-height:48px;height:auto}.timeIntervalItem .description{margin:10px 0;height:20px;line-height:20px;width:100%}.timeIntervalItem .placeholder{color:var(--ion-color-medium)}.timeIntervalItem .selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.timeIntervalItem.withLabel{min-height:58px;height:auto}.timeIntervalItem.withLabel .selectIcon{padding-top:25px}.timeIntervalItem.withLabel ion-button[slot=start]{margin-top:16px}.selectable{cursor:pointer}\n"] }]
2556
+ }], propDecorators: { timeInterval: [{
2557
+ type: Input
2558
+ }], period: [{
2559
+ type: Input
2560
+ }], notEarlierThan: [{
2561
+ type: Input
2562
+ }], notLaterThan: [{
2563
+ type: Input
2564
+ }], label: [{
2565
+ type: Input
2566
+ }], icon: [{
2567
+ type: Input
2568
+ }], iconColor: [{
2569
+ type: Input
2570
+ }], placeholder: [{
2571
+ type: Input
2572
+ }], disabled: [{
2573
+ type: Input
2574
+ }], tappableWhenDisabled: [{
2575
+ type: Input
2576
+ }], obligatory: [{
2577
+ type: Input
2578
+ }], lines: [{
2579
+ type: Input
2580
+ }], color: [{
2581
+ type: Input
2582
+ }], select: [{
2583
+ type: Output
2584
+ }], iconSelect: [{
2585
+ type: Output
2586
+ }], selectWhenDisabled: [{
2587
+ type: Output
2588
+ }] } });
2589
+
2590
+ class IDEATimeIntervalModule {
2591
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
2592
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalModule, declarations: [IDEAFromTimeToTimeComponent, IDEATimeIntervalComponent], imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule], exports: [IDEAFromTimeToTimeComponent, IDEATimeIntervalComponent] }); }
2593
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalModule, imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule] }); }
2594
+ }
2595
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEATimeIntervalModule, decorators: [{
2596
+ type: NgModule,
2597
+ args: [{
2598
+ imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
2599
+ declarations: [IDEAFromTimeToTimeComponent, IDEATimeIntervalComponent],
2600
+ exports: [IDEAFromTimeToTimeComponent, IDEATimeIntervalComponent]
2601
+ }]
2602
+ }] });
2603
+
2604
+ /**
2605
+ * To communicate with an AWS API Gateway websocket istance.
2606
+ *
2607
+ * Note: requires an `AWSAPIAuthToken` variable to be set by IDEATinCanService service.
2608
+ */
2609
+ class IDEAAWSAPISocketService {
2610
+ constructor() {
2611
+ this._env = inject(IDEAEnvironment);
2612
+ this._tc = inject(IDEATinCanService);
2613
+ this._errorReporting = inject(IDEAErrorReportingService);
2614
+ this._offline = inject(IDEAOfflineService);
2615
+ /**
2616
+ * The maximum amount of minutes of inactivity after which we ping the connection to keep it alive.
2617
+ */
2618
+ this.MAX_INACTIVITY_MINUTES = 7;
2619
+ /**
2620
+ * The amount of minutes after which we check if the connection needs to be pinged to be kept alive.
2621
+ */
2622
+ this.KEEP_ALIVE_MINUTES_INTERVAL = 2;
2623
+ /**
2624
+ * In case we are offline, the amount of minutes after which we check if we are online, so we can re-connect.
2625
+ */
2626
+ this.TRY_AGAIN_OFFLINE_MINUTES = 3;
2627
+ this.apiStage = this._env.idea.socket?.stage || this._env.idea.socket?.version;
2628
+ this.WEBSOCKET_API_URL = 'wss://'.concat([this._env.idea.socket?.url, this.apiStage].filter(x => x).join('/'));
2629
+ }
2630
+ /**
2631
+ * Open a connection to the websocket API.
2632
+ */
2633
+ open(options) {
2634
+ this.options = options || this.options || {};
2635
+ // skip in case we have an open and valid connection
2636
+ if (this.connection && this.isOpen())
2637
+ return;
2638
+ // prepare optional query params for the connection/authorization request
2639
+ let qp = '';
2640
+ Object.entries(this.options.openParams || {}).forEach(([key, value]) => (qp += `&${key}=${String(value)}`));
2641
+ // open a connection (with IDEA authentication or not)
2642
+ const auth = this.options.noAuth ? '?noAuth=' : '?Authorization='.concat(this._tc.get('AWSAPIAuthToken'));
2643
+ this.connection = new WebSocket(this.WEBSOCKET_API_URL.concat(auth, qp));
2644
+ // set the event listeners
2645
+ this.connection.onopen = (event) => {
2646
+ if (this.options.debug)
2647
+ console.log('WEBSOCKET IS OPEN', event);
2648
+ // reset the try-again-when-offline mechanism, since we are online and connected
2649
+ if (this.offlineTimeout)
2650
+ clearTimeout(this.offlineTimeout);
2651
+ this.offlineTimeout = null;
2652
+ // mark the start of the connection as the first activity time
2653
+ this.lastActivityAt = Date.now();
2654
+ // set a timer that every N minutes checks if we need to ping the connection to keep it alive
2655
+ this.pingTimeout = setTimeout(() => this.keepItAlive(), this.minutesToMS(this.KEEP_ALIVE_MINUTES_INTERVAL));
2656
+ // set the connection as correctly opened at least once (for re-connecting scenarios)
2657
+ this.wasOpenAtLeastOnce = true;
2658
+ // run the user-defined onOpen actions
2659
+ if (this.options.onOpen)
2660
+ this.options.onOpen(event);
2661
+ };
2662
+ this.connection.onclose = (event) => {
2663
+ if (this.options.debug)
2664
+ console.log('WEBSOCKET WAS CLOSED BY EXTERNAL EVENTS', event);
2665
+ // run the user-defined onClose actions
2666
+ if (this.options.onClose)
2667
+ this.options.onClose(event);
2668
+ // in case the connection was closed by other events (e.g. offline) reset the data and try to re-open the socket
2669
+ this.close(this.wasOpenAtLeastOnce);
2670
+ };
2671
+ this.connection.onerror = (event) => {
2672
+ if (this.options.debug)
2673
+ console.log('WEBSOCKET HAD AN ERROR', event);
2674
+ // send an error report, if requested
2675
+ if (this.options.reportError) {
2676
+ const errMsg = ''.concat(event?.target?.url, ' @@@ ', this._tc.get('user')?.userId || this._tc.get('userId'));
2677
+ this._errorReporting.sendReport(new Error(errMsg));
2678
+ }
2679
+ // run the user-defined onError actions
2680
+ if (this.options.onError)
2681
+ this.options.onError(event);
2682
+ };
2683
+ this.connection.onmessage = (event) => {
2684
+ if (this.options.debug)
2685
+ console.log('WEBSOCKET INCOMING MESSAGE', event);
2686
+ try {
2687
+ const data = JSON.parse(event.data || {});
2688
+ if (this.options.onMessage)
2689
+ this.options.onMessage(data);
2690
+ }
2691
+ catch (err) {
2692
+ /* IGNORE: it probably comes from a keep-alive ping-pong message */
2693
+ }
2694
+ this.lastActivityAt = Date.now();
2695
+ };
2696
+ }
2697
+ /**
2698
+ * Close the current connection to the websocket API.
2699
+ * @param itShouldReopen in case the connection was forcibly closed, try to re-open it (with offline mechanism).
2700
+ */
2701
+ close(itShouldReopen) {
2702
+ // skip in case there isn't an active connection
2703
+ if (!this.connection)
2704
+ return;
2705
+ // reset the events listener
2706
+ this.connection.onopen = null;
2707
+ this.connection.onclose = null;
2708
+ this.connection.onmessage = null;
2709
+ this.connection.onerror = null;
2710
+ // in case the connection was closed by a manual action, reset the initial parameters
2711
+ if (!itShouldReopen) {
2712
+ this.options = null;
2713
+ this.wasOpenAtLeastOnce = false;
2714
+ }
2715
+ // reset the keep-alive mechanism
2716
+ if (this.pingTimeout)
2717
+ clearTimeout(this.pingTimeout);
2718
+ this.pingTimeout = null;
2719
+ this.lastActivityAt = null;
2720
+ // reset the try-again-when-offline mechanism
2721
+ if (this.offlineTimeout)
2722
+ clearTimeout(this.offlineTimeout);
2723
+ this.offlineTimeout = null;
2724
+ // close the connection and empty the reference
2725
+ this.connection.close();
2726
+ this.connection = null;
2727
+ // in case the connection was forcibly closed, try to re-open it
2728
+ if (itShouldReopen)
2729
+ this.reOpen();
2730
+ }
2731
+ /**
2732
+ * Try to-reopen a closed connection, if we are online.
2733
+ */
2734
+ async reOpen() {
2735
+ // in case we are online, reopen the connection right away
2736
+ const isOnline = await this._offline.check();
2737
+ if (this.options.debug)
2738
+ console.log('WEBSOCKET WILL TRY TO RE-OPEN', isOnline, new Date());
2739
+ if (isOnline)
2740
+ return this.open();
2741
+ // if we are offline, set a timer to try to re-open the connection later on
2742
+ this.offlineTimeout = setTimeout(() => this.reOpen(), this.minutesToMS(this.TRY_AGAIN_OFFLINE_MINUTES));
2743
+ }
2744
+ /**
2745
+ * Whether the socket connection is currently open.
2746
+ */
2747
+ isOpen() {
2748
+ return this.connection && this.connection.readyState === WebSocket.OPEN;
2749
+ }
2750
+ /**
2751
+ * A simple message delivered to the server to keep the connection alive.
2752
+ */
2753
+ ping() {
2754
+ if (this.options.debug)
2755
+ console.log('WEBSOCKET IS PINGING THE SERVER', new Date());
2756
+ this.connection.send('ping');
2757
+ }
2758
+ /**
2759
+ * Helper to keep the connection alive, if needed.
2760
+ */
2761
+ keepItAlive() {
2762
+ if (this.options.debug)
2763
+ console.log('WEBSOCKET KEEP-IT-ALIVE', new Date());
2764
+ // if there wasn't an acivity in the last MAX_INACTIVITY_MINUTES, ping the connection
2765
+ if (!this.lastActivityAt || this.lastActivityAt + this.minutesToMS(this.MAX_INACTIVITY_MINUTES) < Date.now())
2766
+ this.ping();
2767
+ // set again the timer to keep the connection alive
2768
+ if (this.pingTimeout)
2769
+ clearTimeout(this.pingTimeout);
2770
+ this.pingTimeout = setTimeout(() => this.keepItAlive(), this.minutesToMS(this.KEEP_ALIVE_MINUTES_INTERVAL));
2771
+ }
2772
+ /**
2773
+ * Convert minutes to milliseconds.
2774
+ */
2775
+ minutesToMS(minutes) {
2776
+ return minutes * 60 * 1000;
2777
+ }
2778
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPISocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2779
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPISocketService }); }
2780
+ }
2781
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAAWSAPISocketService, decorators: [{
2782
+ type: Injectable
2783
+ }], ctorParameters: () => [] });
2784
+
2785
+ /**
2786
+ * *The service aims to solve the resource caching behaviour of browsers.*
2787
+ * *It's suggested to use it with avatars and other images/resources that can change based on user input.*
2788
+ * The browsers tend to cache images based on their URL;
2789
+ * sometimes, the image changes but the URL doesn't, and the browser keeps showing the old image.
2790
+ * Through this service, when we change a resource (e.g. an image), we also update a cache key.
2791
+ * Each time we load a resource through the service's method `getCachedImage`, we use the cache key as a query param;
2792
+ * in this way, the browser uses the same cached images, until we change the cache key.
2793
+ */
2794
+ class IDEACachedResourcesService {
2795
+ constructor() {
2796
+ /**
2797
+ * Change it only if necessary.
2798
+ */
2799
+ this.cacheKeyIndex = 'IDEA_CACHED_RESOURCES_KEY';
2800
+ this.storage = inject(IDEAStorageService);
2801
+ this.loadKeyFromStorage();
2802
+ }
2803
+ /**
2804
+ * Load the cache key from the local storage.
2805
+ */
2806
+ loadKeyFromStorage() {
2807
+ this.storage.get(this.cacheKeyIndex).then(key => {
2808
+ if (key)
2809
+ this.cacheKey = key;
2810
+ else
2811
+ this.updateCacheKey(Date.now());
2812
+ });
2813
+ }
2814
+ /**
2815
+ * Get an image using the service's caching mechanism.
2816
+ * Optionally update the cache key and forces the image's cache deletion.
2817
+ */
2818
+ getCachedResource(url, updateCacheKey) {
2819
+ const key = updateCacheKey ? this.updateCacheKey() : this.cacheKey;
2820
+ return url.concat(`?crs=${key}`);
2821
+ }
2822
+ /**
2823
+ * Set a new cache key.
2824
+ */
2825
+ async updateCacheKey(key = Date.now()) {
2826
+ this.cacheKey = String(key);
2827
+ await this.storage.set(this.cacheKeyIndex, key);
2828
+ }
2829
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACachedResourcesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2830
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACachedResourcesService, providedIn: 'root' }); }
2831
+ }
2832
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEACachedResourcesService, decorators: [{
2833
+ type: Injectable,
2834
+ args: [{ providedIn: 'root' }]
2835
+ }], ctorParameters: () => [] });
2836
+
2837
+ /**
2838
+ * To subscribe push notifications events.
2839
+ */
2840
+ class IDEAPushNotificationsService {
2841
+ constructor() {
2842
+ this._platform = inject(Platform);
2843
+ this._errorReporting = inject(IDEAErrorReportingService);
2844
+ if (!this.isAvailable())
2845
+ return;
2846
+ this.registrations = new Observable(observer => {
2847
+ PushNotifications.addListener('registration', token => observer.next(new PushNotificationsDevice({ token: token.value, platform: PushNotificationsPlatforms.FCM })));
2848
+ });
2849
+ this.errors = new Observable(observer => {
2850
+ // rif. https://forum.ionicframework.com/t/226376
2851
+ PushNotifications.addListener('registrationError', (err) => {
2852
+ this._errorReporting.sendReport(err);
2853
+ observer.next(err);
2854
+ });
2855
+ });
2856
+ this.notifications = new Observable(observer => {
2857
+ PushNotifications.addListener('pushNotificationReceived', n => observer.next(n));
2858
+ });
2859
+ }
2860
+ /**
2861
+ * Return true if the device is supported.
2862
+ */
2863
+ isAvailable() {
2864
+ return this._platform.is('capacitor') && !!PushNotifications;
2865
+ }
2866
+ /**
2867
+ * To fire the registration of a new device: it will rewquest the permission to use Push Notification.
2868
+ * On iOS will prompt the request, on Android it's automatic.
2869
+ */
2870
+ async registerDevice() {
2871
+ if (!this.isAvailable())
2872
+ return;
2873
+ let permStatus = await PushNotifications.checkPermissions();
2874
+ if (permStatus.receive === 'prompt')
2875
+ permStatus = await PushNotifications.requestPermissions();
2876
+ if (permStatus.receive === 'granted')
2877
+ PushNotifications.register();
2878
+ }
2879
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAPushNotificationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2880
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAPushNotificationsService }); }
2881
+ }
2882
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: IDEAPushNotificationsService, decorators: [{
2883
+ type: Injectable
2884
+ }], ctorParameters: () => [] });
2885
+
2886
+ /**
2887
+ * Generated bundle index. Do not edit.
2888
+ */
2889
+
2890
+ export { APIRequestOption, CacheModes, CacheableResource, ColorsPaletteComponent, IDEAAWSAPIService, IDEAAWSAPISocketService, IDEAAddressComponent, IDEAAddressModule, IDEAAnnouncementComponent, IDEAAnnouncementModule, IDEACacheableResourceComponent, IDEACachedResourcesService, IDEACalendarPickerComponent, IDEAColorPickerComponent, IDEAColorPickerModule, IDEAContactsComponent, IDEAContactsModule, IDEADateTimeComponent, IDEADateTimeModule, IDEADurationComponent, IDEADurationModule, IDEAEchoModule, IDEAErrorReportingService, IDEAFromTimeToTimeComponent, IDEAMDEComponent, IDEAMDEModule, IDEAMDEToolbarComponent, IDEAOfflineDataService, IDEAOfflineIndicatorComponent, IDEAOfflineManagerComponent, IDEAOfflineModule, IDEAOfflineService, IDEAPushNotificationsService, IDEASentimentComponent, IDEASentimentModule, IDEATimeIntervalComponent, IDEATimeIntervalModule, IDEATinCanService, LAST_SYNC_KEY, NUM_ELEMENTS_WITHOUT_MANUAL_SYNC, NUM_ELEMENTS_WITH_MANUAL_SYNC, Periods, QUEUE_API_REQUESTS_KEY, SYNC_EXPIRATION_INTERVAL };
2891
+ //# sourceMappingURL=idea-ionic-uncommon.mjs.map