@idea-ionic/uncommon 8.6.6 → 8.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,20 @@
1
1
  import * as i1$1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { EventEmitter, ViewChild, Input, Output, Component, Injectable, inject } from '@angular/core';
4
+ import { EventEmitter, ViewChild, Input, Output, Component, inject, Injectable } from '@angular/core';
5
5
  import * as i1 from '@angular/forms';
6
6
  import { FormsModule } from '@angular/forms';
7
- import { IonText, IonList, IonInput, IonItem, IonAccordion, IonAccordionGroup, IonButton, IonCardContent, IonCard, PopoverController, IonAvatar, IonLabel, IonIcon, IonContent, IonGrid, IonRow, IonCol, AlertController, ModalController, IonFooter, IonButtons, IonTitle, IonToolbar, IonHeader, Platform, IonCardTitle, IonCardHeader, IonImg, IonTextarea, IonSpinner, IonListHeader, IonFab, IonFabButton, IonBadge, IonSegmentButton, IonSegment } from '@ionic/angular/standalone';
8
- import { Countries, Suggestion, mdToHtml, COLORS, Contacts, ErrorReport, ClientInfo, ExternalCalendarSources, Sentiment, TimeInterval, PushNotificationsDevice, PushNotificationsPlatforms } from 'idea-toolbox';
9
- import { IDEASelectComponent, IDEATranslatePipe, IDEAEnvironment, IDEAStorageService, IDEATranslationsService, IDEALocalizedDatePipe, IDEALoadingService, IDEAActionSheetController } from '@idea-ionic/common';
10
- import { ActivatedRoute } from '@angular/router';
7
+ import { IonText, IonList, IonInput, IonItem, IonAccordion, IonAccordionGroup, Platform, IonButton, IonIcon, IonLabel, IonSpinner, IonCardContent, IonCard, PopoverController, IonAvatar, IonContent, IonGrid, IonRow, IonCol, AlertController, ModalController, IonFooter, IonButtons, IonTitle, IonToolbar, IonHeader, IonCardTitle, IonCardHeader, IonImg, IonTextarea, IonListHeader, IonFab, IonFabButton, IonBadge, IonSegmentButton, IonSegment } from '@ionic/angular/standalone';
8
+ import { Countries, Suggestion, ErrorReport, ClientInfo, Attachment, mdToHtml, COLORS, Contacts, ExternalCalendarSources, Sentiment, TimeInterval, PushNotificationsDevice, PushNotificationsPlatforms } from 'idea-toolbox';
9
+ import { IDEASelectComponent, IDEATranslatePipe, IDEAEnvironment, IDEAStorageService, IDEALoadingService, IDEAMessageService, IDEATranslationsService, IDEALocalizedDatePipe, IDEAActionSheetController } from '@idea-ionic/common';
10
+ import heic2any from 'heic2any';
11
+ import { Platform as Platform$1 } from '@ionic/angular';
12
+ import { Browser } from '@capacitor/browser';
13
+ import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
11
14
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
12
15
  import { Observable } from 'rxjs';
13
16
  import { Network } from '@capacitor/network';
14
- import { Browser } from '@capacitor/browser';
17
+ import { ActivatedRoute } from '@angular/router';
15
18
  import { PushNotifications } from '@capacitor/push-notifications';
16
19
 
17
20
  class IDEAAddressComponent {
@@ -415,6 +418,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImpor
415
418
  args: ['accordion']
416
419
  }] } });
417
420
 
421
+ class IDEAErrorReportingService {
422
+ constructor() {
423
+ this._env = inject(IDEAEnvironment);
424
+ this._http = inject(HttpClient);
425
+ this._platform = inject(Platform);
426
+ this.apiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
427
+ this.apiUrl = 'https://'.concat([this._env.idea.ideaApi?.url, this.apiStage].filter(x => x).join('/'));
428
+ }
429
+ /**
430
+ * Send the error report to the back-end.
431
+ */
432
+ sendReport(error, forceSend) {
433
+ return new Promise(resolve => {
434
+ // skip in case we don't want to send the report or the report is useless (blank error)
435
+ if ((!this.shouldSend() && !forceSend) || !error?.name)
436
+ return resolve();
437
+ // prepare and send the report
438
+ const report = new ErrorReport({
439
+ version: this._env.idea.app.version,
440
+ stage: this.apiStage,
441
+ client: this.getClientInfo(),
442
+ type: error.name,
443
+ error: error.message,
444
+ stack: error.stack
445
+ });
446
+ this._http
447
+ .post(this.apiUrl.concat(`/projects/${this._env.idea.project}/errorReporting`), report)
448
+ .toPromise()
449
+ .catch(() => { }) // note: never throw an error when reporting an error
450
+ .finally(() => resolve());
451
+ });
452
+ }
453
+ /**
454
+ * Whether we should send the reporting or we are in a scenario in which we should skip it.
455
+ */
456
+ shouldSend() {
457
+ return this.apiStage === 'prod';
458
+ }
459
+ /**
460
+ * Acquire the client's info.
461
+ */
462
+ getClientInfo() {
463
+ return new ClientInfo({
464
+ timestamp: new Date().toISOString(),
465
+ platform: this._platform.platforms().join(' '),
466
+ screenWidth: this._platform.width(),
467
+ screenHeight: this._platform.height(),
468
+ isLandScape: this._platform.isLandscape(),
469
+ url: this._platform.url(),
470
+ referrer: document.referrer,
471
+ isOnline: navigator.onLine,
472
+ language: navigator.language,
473
+ userAgent: navigator.userAgent
474
+ });
475
+ }
476
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
477
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, providedIn: 'root' }); }
478
+ }
479
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, decorators: [{
480
+ type: Injectable,
481
+ args: [{ providedIn: 'root' }]
482
+ }], ctorParameters: () => [] });
483
+
418
484
  /**
419
485
  * @deprecated use a specific App service instead.
420
486
  */
@@ -463,1584 +529,1887 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImpor
463
529
  }], ctorParameters: () => [] });
464
530
 
465
531
  /**
466
- * Announcement card that can be set in any interface to show the alert for this project.
532
+ * Utility to monitor the network connection.
533
+ * Note: don't use it directly, if you're also using IDEAOfflineDataService.
467
534
  */
468
- class IDEAAnnouncementComponent {
535
+ class IDEAOfflineService {
469
536
  constructor() {
470
- this._env = inject(IDEAEnvironment);
471
- this._tc = inject(IDEATinCanService);
472
- this._storage = inject(IDEAStorageService);
473
- this.storageKey = (this._env.idea.project || 'app').concat('_LAST_ANNOUNCEMENT');
474
- this.color = 'dark';
475
- }
476
- async ngOnInit() {
477
- // standard IDEA: the announcement is read in the Init Guard
478
- this.announcement = this._tc.get('idea-announcement');
479
- if (this.announcement && this.announcement.content) {
480
- const lastRead = await this._storage.get(this.storageKey);
481
- if (!lastRead || this.announcement.content !== lastRead)
482
- this.htmlContent = mdToHtml(this.announcement.content);
537
+ // get an instant status
538
+ this._isOffline = !navigator.onLine;
539
+ if (Network) {
540
+ // set the basic listener for changes
541
+ this.listenerHandler = Network.addListener('networkStatusChange', status => (this._isOffline = !status.connected));
542
+ // create the observable to subscribe to the network changes; note: it won't run until subscribed
543
+ this.observable = new Observable(observer => {
544
+ // remove the basic listener add add one that supports a subscription
545
+ Network.removeAllListeners();
546
+ this.listenerHandler = Network.addListener('networkStatusChange', status => {
547
+ this._isOffline = !status.connected;
548
+ if (observer)
549
+ observer.next(status.connected);
550
+ });
551
+ });
483
552
  }
484
553
  }
485
- async markAsRead() {
486
- await this._storage.set(this.storageKey, this.announcement.content);
487
- this.htmlContent = null;
554
+ /**
555
+ * Subscribe to be notified when the connection status changes.
556
+ */
557
+ subscribe(callback) {
558
+ return this.observable.subscribe(callback);
488
559
  }
489
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAnnouncementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
490
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAAnnouncementComponent, isStandalone: true, selector: "idea-announcement", inputs: { color: "color" }, ngImport: i0, template: `
491
- @if (htmlContent) {
492
- <ion-card class="announcementCard" [color]="color">
493
- <ion-card-content>
494
- <p class="htmlContent" [innerHTML]="htmlContent"></p>
495
- <p class="ion-text-right">
496
- <ion-button color="primary" (click)="markAsRead()">
497
- {{ 'COMMON.GOT_IT' | translate }}
498
- </ion-button>
499
- </p>
500
- </ion-card-content>
501
- </ion-card>
560
+ /**
561
+ * Quickly check the connection status.
562
+ */
563
+ isOnline() {
564
+ return !this._isOffline;
502
565
  }
503
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: 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: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
504
- }
505
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAnnouncementComponent, decorators: [{
506
- type: Component,
507
- args: [{
508
- selector: 'idea-announcement',
509
- standalone: true,
510
- imports: [CommonModule, IDEATranslatePipe, IonButton, IonCardContent, IonCard],
511
- template: `
512
- @if (htmlContent) {
513
- <ion-card class="announcementCard" [color]="color">
514
- <ion-card-content>
515
- <p class="htmlContent" [innerHTML]="htmlContent"></p>
516
- <p class="ion-text-right">
517
- <ion-button color="primary" (click)="markAsRead()">
518
- {{ 'COMMON.GOT_IT' | translate }}
519
- </ion-button>
520
- </p>
521
- </ion-card-content>
522
- </ion-card>
566
+ /**
567
+ * Quickly check the connection status.
568
+ */
569
+ isOffline() {
570
+ return this._isOffline;
523
571
  }
524
- `
525
- }]
526
- }], ctorParameters: () => [], propDecorators: { color: [{
527
- type: Input
528
- }] } });
572
+ /**
573
+ * Quickly check for online connection.
574
+ */
575
+ async check() {
576
+ if (!Network)
577
+ return false;
578
+ const status = await Network.getStatus();
579
+ this._isOffline = !status.connected;
580
+ return status.connected;
581
+ }
582
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
583
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, providedIn: 'root' }); }
584
+ }
585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, decorators: [{
586
+ type: Injectable,
587
+ args: [{ providedIn: 'root' }]
588
+ }], ctorParameters: () => [] });
529
589
 
530
590
  /**
531
- * Pick a color from a defined set.
591
+ * To communicate with an AWS's API Gateway istance.
592
+ *
593
+ * Includes a rundimental cache on GET requests, until Android doesn't support SWs on local files.
594
+ *
595
+ * Note: requires an `AWSAPIAuthToken` variable to be set by IDEATinCanService service.
532
596
  */
533
- class IDEAColorPickerComponent {
597
+ class IDEAAWSAPIService {
534
598
  constructor() {
535
- /**
536
- * The pickable colors.
537
- */
538
- this.colors = COLORS;
539
- /**
540
- * On select event.
541
- */
542
- this.select = new EventEmitter();
543
- /**
544
- * Icon select.
545
- */
546
- this.iconSelect = new EventEmitter();
547
- this._popover = inject(PopoverController);
548
- }
549
- async openPalette(event) {
550
- if (this.disabled)
551
- return;
552
- const componentProps = { colors: this.colors, current: this.current };
553
- const popover = await this._popover.create({ component: ColorsPaletteComponent, componentProps, event });
554
- popover.onDidDismiss().then(res => {
555
- if (res && res.data)
556
- this.select.emit(res.data);
557
- });
558
- popover.present();
559
- }
560
- doIconSelect(event) {
561
- if (event)
562
- event.stopPropagation();
563
- this.iconSelect.emit(event);
599
+ this._env = inject(IDEAEnvironment);
600
+ this._http = inject(HttpClient);
601
+ this._platform = inject(Platform);
602
+ this._tc = inject(IDEATinCanService);
603
+ this._storage = inject(IDEAStorageService);
604
+ this._errorReporting = inject(IDEAErrorReportingService);
605
+ this._offline = inject(IDEAOfflineService);
606
+ this.apiStage = this._env.idea.api?.stage || this._env.idea.api?.version;
607
+ this.apiUrlProject = 'https://'.concat([this._env.idea.api?.url, this.apiStage].filter(x => x).join('/'));
608
+ this.ideaApiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
609
+ this.apiUrlIDEA = 'https://'.concat([this._env.idea.ideaApi?.url, this.ideaApiStage].filter(x => x).join('/'));
564
610
  }
565
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
566
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAColorPickerComponent, isStandalone: true, 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: `
567
- <ion-item
568
- class="colorPickerItem"
569
- [color]="color"
570
- [lines]="lines"
571
- [button]="!disabled"
572
- [title]="placeholder || ''"
573
- [class.withLabel]="label"
574
- (click)="openPalette($event)"
575
- >
576
- @if (icon) {
577
- <ion-button fill="clear" slot="start" [color]="iconColor" (click)="doIconSelect($event)">
578
- <ion-icon [name]="icon" slot="icon-only" />
579
- </ion-button>
580
- }
581
- @if (label) {
582
- <ion-label [class.selectable]="!disabled">
583
- {{ label }}
584
- @if (obligatory && !disabled) {
585
- <ion-text class="obligatoryDot" />
586
- }
587
- </ion-label>
588
- }
589
- <ion-avatar slot="end" class="colorCircle" [style.background-color]="current" />
590
- @if (!disabled) {
591
- <ion-icon slot="end" name="caret-down" class="selectIcon" [class.selectable]="!disabled" />
592
- }
593
- </ion-item>
594
- `, isInline: true, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonAvatar, selector: "ion-avatar" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }] }); }
595
- }
596
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAColorPickerComponent, decorators: [{
597
- type: Component,
598
- args: [{ selector: 'idea-color-picker', standalone: true, imports: [CommonModule, IonAvatar, IonText, IonLabel, IonIcon, IonButton, IonItem], template: `
599
- <ion-item
600
- class="colorPickerItem"
601
- [color]="color"
602
- [lines]="lines"
603
- [button]="!disabled"
604
- [title]="placeholder || ''"
605
- [class.withLabel]="label"
606
- (click)="openPalette($event)"
607
- >
608
- @if (icon) {
609
- <ion-button fill="clear" slot="start" [color]="iconColor" (click)="doIconSelect($event)">
610
- <ion-icon [name]="icon" slot="icon-only" />
611
- </ion-button>
612
- }
613
- @if (label) {
614
- <ion-label [class.selectable]="!disabled">
615
- {{ label }}
616
- @if (obligatory && !disabled) {
617
- <ion-text class="obligatoryDot" />
618
- }
619
- </ion-label>
620
- }
621
- <ion-avatar slot="end" class="colorCircle" [style.background-color]="current" />
622
- @if (!disabled) {
623
- <ion-icon slot="end" name="caret-down" class="selectIcon" [class.selectable]="!disabled" />
624
- }
625
- </ion-item>
626
- `, 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"] }]
627
- }], propDecorators: { colors: [{
628
- type: Input
629
- }], current: [{
630
- type: Input
631
- }], label: [{
632
- type: Input
633
- }], placeholder: [{
634
- type: Input
635
- }], icon: [{
636
- type: Input
637
- }], iconColor: [{
638
- type: Input
639
- }], disabled: [{
640
- type: Input
641
- }], obligatory: [{
642
- type: Input
643
- }], lines: [{
644
- type: Input
645
- }], color: [{
646
- type: Input
647
- }], select: [{
648
- type: Output
649
- }], iconSelect: [{
650
- type: Output
651
- }] } });
652
- /**
653
- * Component to open in a popover for displaying the colors available.
654
- */
655
- class ColorsPaletteComponent {
656
- constructor() {
657
- this._popover = inject(PopoverController);
611
+ /**
612
+ * Execute an online API request.
613
+ * @param resource resource name (e.g. `users`)
614
+ * @param method enum HTTP methods
615
+ * @param options the request options
616
+ */
617
+ request(resource, method, options) {
618
+ return new Promise((resolve, reject) => {
619
+ const opt = options || {};
620
+ // prepare the url
621
+ const url = this.prepareURL(resource, opt);
622
+ // prepare the headers and set the Authorization token
623
+ const headers = this.prepareHeaders(opt.headers);
624
+ // prepare the query params and add some info about the client
625
+ const params = this.prepareQueryParams(opt.params, true);
626
+ // execute the request
627
+ let req;
628
+ switch (method) {
629
+ case 'HEAD':
630
+ req = this._http.head(url, { headers, params });
631
+ break;
632
+ case 'POST':
633
+ req = this._http.post(url, opt.body, { headers, params });
634
+ break;
635
+ case 'PUT':
636
+ req = this._http.put(url, opt.body, { headers, params });
637
+ break;
638
+ case 'PATCH':
639
+ req = this._http.patch(url, opt.body, { headers, params });
640
+ break;
641
+ case 'DELETE':
642
+ req = this._http.delete(url, { headers, params });
643
+ break;
644
+ case 'GET':
645
+ default:
646
+ req = this._http.get(url, { headers, params });
647
+ }
648
+ // handle the request response
649
+ req.subscribe((res) => resolve(res), async (err) => {
650
+ // check if the request failed for network reasons (to trigger offline mode if needed)
651
+ await this._offline.check();
652
+ // (async) send a report, if wanted
653
+ if (opt.reportError)
654
+ this._errorReporting.sendReport(err);
655
+ // fix and return the error
656
+ this.fixErrMessageBeforeReject(err, reject);
657
+ });
658
+ });
659
+ }
660
+ /**
661
+ * Prepare the URL to which execute the request.
662
+ */
663
+ prepareURL(resource, opt) {
664
+ // decide whether to use IDEA's API or the project's API (or an alternative API)
665
+ let url = opt.idea ? this.apiUrlIDEA : opt.alternativeAPI || this.apiUrlProject;
666
+ // prepare a single-resource request (by id) or a normal one
667
+ url = `${url}/${resource}`;
668
+ if (opt.resourceId)
669
+ url = `${url}/${encodeURIComponent(opt.resourceId)}`;
670
+ return url;
671
+ }
672
+ /**
673
+ * Prepare the headers object to send with a request.
674
+ * Note: it always adds the Authorization token (`AWSAPIAuthToken`) and the API key (`AWSAPIKey`), if set.
675
+ */
676
+ prepareHeaders(h) {
677
+ // preare the headers; note: HttpHeaders is immutable!
678
+ let headers = new HttpHeaders(h || null);
679
+ // set the Authorization token
680
+ if (!headers.get('Authorization') && this._tc.get('AWSAPIAuthToken'))
681
+ headers = headers.set('Authorization', this._tc.get('AWSAPIAuthToken'));
682
+ // set the API key
683
+ if (!headers.get('X-API-Key') && this._tc.get('AWSAPIKey'))
684
+ headers = headers.set('X-API-Key', this._tc.get('AWSAPIKey'));
685
+ return headers;
686
+ }
687
+ /**
688
+ * Prepare the queryParams object to send with a request; if requested, add some info about the client.
689
+ */
690
+ prepareQueryParams(qp, addClientInfo) {
691
+ // prepare the query params; note: HttpParams is immutable!
692
+ let searchParams = new HttpParams();
693
+ if (qp && qp instanceof HttpParams)
694
+ searchParams = qp;
695
+ else if (qp)
696
+ for (const prop in qp)
697
+ if (qp[prop])
698
+ searchParams = searchParams.set(prop, qp[prop]);
699
+ // if requested, add app version and client platform to the info we send to the back-end
700
+ if (addClientInfo) {
701
+ searchParams = searchParams.set('_v', this._env.idea.app.version);
702
+ searchParams = searchParams.set('_p', this._platform.platforms().join(' '));
703
+ }
704
+ return searchParams;
705
+ }
706
+ /**
707
+ * Converts the error message to be readable.
708
+ */
709
+ fixErrMessageBeforeReject(err, reject) {
710
+ let e;
711
+ try {
712
+ if (!err || !err.error)
713
+ e = {};
714
+ else if (typeof err.error === 'string')
715
+ e = JSON.parse(err.error);
716
+ else
717
+ e = err.error;
718
+ }
719
+ catch (_) {
720
+ e = {};
721
+ }
722
+ reject(new Error(e.message || 'Unknown error!'));
723
+ }
724
+ /**
725
+ * Use the HttpClient to execute a raw request.
726
+ */
727
+ rawRequest() {
728
+ return this._http;
729
+ }
730
+ /**
731
+ * HEAD request.
732
+ * @param resource resource name (e.g. `users`)
733
+ * @param options the request options
734
+ */
735
+ headResource(resource, options) {
736
+ return this.request(resource, 'HEAD', options);
737
+ }
738
+ /**
739
+ * GET request. Include options to cache the requests (`options.useCache`), to work also offline.
740
+ * @param resource resource name (e.g. `users`)
741
+ * @param options the request options
742
+ */
743
+ getResource(resource, options) {
744
+ return new Promise((resolve, reject) => {
745
+ const opt = options || {};
746
+ // if offline and with a cache mode set, force the request to the cache
747
+ if (!navigator.onLine && opt.useCache)
748
+ opt.useCache = CacheModes.CACHE_ONLY;
749
+ // execute the GET request online or through the cache, depending on the chosen mode
750
+ switch (opt.useCache) {
751
+ case CacheModes.CACHE_ONLY:
752
+ // return the result from the cache, without retrying online if a result wasn't found
753
+ this.getFromCache(resource, opt).then((res) => {
754
+ if (res)
755
+ resolve(res);
756
+ else
757
+ reject();
758
+ });
759
+ break;
760
+ case CacheModes.CACHE_FIRST:
761
+ // return the result from the cache
762
+ this.getFromCache(resource, opt).then((localRes) => {
763
+ // check whether the local resource is present (element or list)
764
+ if (localRes && (opt.resourceId || localRes.length)) {
765
+ resolve(localRes);
766
+ // asynchrounously execute the request online, to update the cache with latest data
767
+ this.request(resource, 'GET', opt)
768
+ .then((cloudRes) => {
769
+ // update the cache (if it fails, it's ok)
770
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
771
+ })
772
+ .catch(() => { }); // we already returned the result, an error is acceptable
773
+ }
774
+ else {
775
+ // if the result isn't found in the cache, return the result of the online request
776
+ this.request(resource, 'GET', opt)
777
+ .then((res) => {
778
+ resolve(res);
779
+ // update the cache (if it fails, it's ok)
780
+ this.putInCache(resource, res, opt).catch(() => { });
781
+ })
782
+ .catch((err) => reject(err)); // element not found
783
+ }
784
+ });
785
+ break;
786
+ case CacheModes.NETWORK_FIRST:
787
+ // return the result from an online request
788
+ this.request(resource, 'GET', opt)
789
+ .then((cloudRes) => {
790
+ resolve(cloudRes);
791
+ // asynchrounously get the same element from cache and decide whether to update or not
792
+ this.getFromCache(resource, opt).then(() => {
793
+ // update the cache (if it fails, it's ok)
794
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
795
+ });
796
+ })
797
+ .catch(() => {
798
+ // if a request to the network fails, check in cache
799
+ this.getFromCache(resource, opt).then((res) => {
800
+ if (res)
801
+ resolve(res);
802
+ else
803
+ reject();
804
+ });
805
+ });
806
+ break;
807
+ default:
808
+ /* CacheModes.NO_CACHE */
809
+ // return the result from an online request, with no cache involved
810
+ this.request(resource, 'GET', opt)
811
+ .then((res) => resolve(res))
812
+ .catch((err) => reject(err));
813
+ }
814
+ });
815
+ }
816
+ /**
817
+ * Observe a GET request. Include options to cache the requests (`options.useCache`), to work also offline.
818
+ * @param resource resource name (e.g. `users`)
819
+ * @param options the request options
820
+ */
821
+ getResourceObserver(resource, options) {
822
+ return new Observable(observer => {
823
+ const opt = options || {};
824
+ // if offline and with a cache mode set, force the request to the cache
825
+ if (!navigator.onLine && opt.useCache)
826
+ opt.useCache = CacheModes.CACHE_ONLY;
827
+ // execute the GET request online or through the cache, depending on the chosen mode
828
+ switch (opt.useCache) {
829
+ case CacheModes.CACHE_ONLY:
830
+ // return the result from the cache, without retrying online if a result wasn't found
831
+ this.getFromCache(resource, opt).then((res) => {
832
+ if (res)
833
+ observer.next(res);
834
+ else
835
+ observer.error();
836
+ });
837
+ break;
838
+ case CacheModes.CACHE_FIRST:
839
+ // return the result from the cache
840
+ this.getFromCache(resource, opt).then((localRes) => {
841
+ if (localRes) {
842
+ observer.next(localRes);
843
+ // asynchrounously execute the request online, to update the cache with latest data
844
+ this.request(resource, 'GET', opt)
845
+ .then((cloudRes) => {
846
+ observer.next(cloudRes);
847
+ // update the cache (if it fails, it's ok)
848
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
849
+ })
850
+ .catch(() => { }); // we already returned the result, an error is acceptable
851
+ }
852
+ else {
853
+ // if the result isn't found in the cache, return the result of the online request
854
+ this.request(resource, 'GET', opt)
855
+ .then((res) => {
856
+ observer.next(res);
857
+ // update the cache (if it fails, it's ok)
858
+ this.putInCache(resource, res, opt).catch(() => { });
859
+ })
860
+ .catch((err) => observer.error(err)); // element not found
861
+ }
862
+ });
863
+ break;
864
+ case CacheModes.NETWORK_FIRST:
865
+ // return the result from an online request
866
+ this.request(resource, 'GET', opt)
867
+ .then((cloudRes) => {
868
+ observer.next(cloudRes);
869
+ // update the cache (if it fails, it's ok)
870
+ this.putInCache(resource, cloudRes, opt).catch(() => { });
871
+ })
872
+ .catch(() => {
873
+ // if a request to the network fails, check in cache
874
+ this.getFromCache(resource, opt).then((res) => {
875
+ if (res)
876
+ observer.next(res);
877
+ else
878
+ observer.error();
879
+ });
880
+ });
881
+ break;
882
+ default:
883
+ /* CacheModes.NO_CACHE */
884
+ // return the result from an online request, with no cache involved
885
+ this.request(resource, 'GET', opt)
886
+ .then((res) => observer.next(res))
887
+ .catch((err) => observer.error(err));
888
+ }
889
+ });
890
+ }
891
+ /**
892
+ * Execute a GET request from the local storage.
893
+ * @param resource resource name (e.g. `users`)
894
+ * @param options the request options
895
+ */
896
+ getFromCache(resource, options) {
897
+ return new Promise(resolve => {
898
+ const opt = options || {};
899
+ // prepare the url and the query params
900
+ const url = this.prepareURL(resource, opt);
901
+ const queryParams = this.prepareQueryParams(opt.params);
902
+ // get from storage, by the complete url
903
+ this._storage
904
+ .get(url.concat(queryParams.toString()))
905
+ .then((res) => (res ? resolve(res) : opt.resourceId ? resolve(null) : resolve([])));
906
+ });
658
907
  }
659
- pick(color) {
660
- this._popover.dismiss(color);
908
+ /**
909
+ * Execute a PUT request in the local storage.
910
+ * @param resource resource name (e.g. `users`)
911
+ * @param data resource data
912
+ * @param options the request options
913
+ */
914
+ putInCache(resource, data, options) {
915
+ return new Promise((resolve, reject) => {
916
+ const opt = options || {};
917
+ // prepare the url and the query params
918
+ const url = this.prepareURL(resource, opt);
919
+ const queryParams = this.prepareQueryParams(opt.params);
920
+ // put in the storage, by the complete url
921
+ this._storage
922
+ .set(url.concat(queryParams.toString()), data)
923
+ .then(() => resolve())
924
+ .catch((err) => reject(err));
925
+ });
661
926
  }
662
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ColorsPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
663
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: ColorsPaletteComponent, isStandalone: true, selector: "idea-colors-palette", inputs: { colors: "colors", current: "current" }, ngImport: i0, template: `
664
- <ion-content>
665
- <ion-grid>
666
- <ion-row>
667
- @for (c of colors; track c) {
668
- <ion-col [size]="2">
669
- <ion-avatar
670
- class="colorCircle tappable"
671
- [style.background-color]="c.hex"
672
- [title]="c.name || c.hex"
673
- (click)="pick(c.hex)"
674
- >
675
- @if (c.hex === current) {
676
- <ion-icon name="checkmark" />
677
- }
678
- </ion-avatar>
679
- </ion-col>
680
- }
681
- </ion-row>
682
- </ion-grid>
683
- </ion-content>
684
- `, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonRow, selector: "ion-row" }, { kind: "component", type: 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: IonAvatar, selector: "ion-avatar" }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
927
+ /**
928
+ * Execute a DELETE request in the local storage.
929
+ * @param resource resource name (e.g. `users`)
930
+ * @param options the request options
931
+ */
932
+ deleteFromCache(resource, options) {
933
+ return new Promise((resolve, reject) => {
934
+ const opt = options || {};
935
+ // prepare the url and the query params
936
+ const url = this.prepareURL(resource, opt);
937
+ const queryParams = this.prepareQueryParams(opt.params);
938
+ // delete from storage, by the complete url
939
+ this._storage
940
+ .remove(url.concat(queryParams.toString()))
941
+ .then(() => resolve())
942
+ .catch((err) => reject(err));
943
+ });
944
+ }
945
+ /**
946
+ * POST request.
947
+ * @param resource resource name (e.g. `users`)
948
+ * @param options the request options
949
+ */
950
+ postResource(resource, options) {
951
+ return this.request(resource, 'POST', options);
952
+ }
953
+ /**
954
+ * PUT request.
955
+ * @param resource resource name (e.g. `users`)
956
+ * @param options the request options
957
+ */
958
+ putResource(resource, options) {
959
+ return this.request(resource, 'PUT', options);
960
+ }
961
+ /**
962
+ * PATCH request.
963
+ * @param resource resource name (e.g. `users`)
964
+ * @param options the request options
965
+ */
966
+ patchResource(resource, options) {
967
+ return this.request(resource, 'PATCH', options);
968
+ }
969
+ /**
970
+ * DELETE request.
971
+ * @param resource resource name (e.g. `users`)
972
+ * @param options the request options
973
+ */
974
+ deleteResource(resource, options) {
975
+ return this.request(resource, 'DELETE', options);
976
+ }
977
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
978
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, providedIn: 'root' }); }
685
979
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ColorsPaletteComponent, decorators: [{
687
- type: Component,
688
- args: [{ selector: 'idea-colors-palette', standalone: true, imports: [CommonModule, IonContent, IonGrid, IonRow, IonCol, IonAvatar, IonIcon], template: `
689
- <ion-content>
690
- <ion-grid>
691
- <ion-row>
692
- @for (c of colors; track c) {
693
- <ion-col [size]="2">
694
- <ion-avatar
695
- class="colorCircle tappable"
696
- [style.background-color]="c.hex"
697
- [title]="c.name || c.hex"
698
- (click)="pick(c.hex)"
699
- >
700
- @if (c.hex === current) {
701
- <ion-icon name="checkmark" />
702
- }
703
- </ion-avatar>
704
- </ion-col>
705
- }
706
- </ion-row>
707
- </ion-grid>
708
- </ion-content>
709
- `, 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"] }]
710
- }], propDecorators: { colors: [{
711
- type: Input
712
- }], current: [{
713
- type: Input
714
- }] } });
980
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, decorators: [{
981
+ type: Injectable,
982
+ args: [{ providedIn: 'root' }]
983
+ }], ctorParameters: () => [] });
984
+ /**
985
+ * The options of an API request.
986
+ */
987
+ class APIRequestOption {
988
+ }
989
+ /**
990
+ * Set of usable cache modes.
991
+ */
992
+ var CacheModes;
993
+ (function (CacheModes) {
994
+ /**
995
+ * Return the result from an online request, with no cache involved.
996
+ */
997
+ CacheModes[CacheModes["NO_CACHE"] = 0] = "NO_CACHE";
998
+ /**
999
+ * Return the result from an online request, storing the result in the cache, to update it with the latest data.
1000
+ */
1001
+ CacheModes[CacheModes["NETWORK_FIRST"] = 1] = "NETWORK_FIRST";
1002
+ /**
1003
+ * Return the result from the cache, but also execute the request online, to update the cache with the latest data.
1004
+ */
1005
+ CacheModes[CacheModes["CACHE_FIRST"] = 2] = "CACHE_FIRST";
1006
+ /**
1007
+ * Return the result from the cache, without retrying online if a result wasn't found.
1008
+ */
1009
+ CacheModes[CacheModes["CACHE_ONLY"] = 3] = "CACHE_ONLY";
1010
+ })(CacheModes || (CacheModes = {}));
715
1011
 
716
- class IDEAContactsComponent {
1012
+ class IDEAOldAttachmentsComponent {
717
1013
  constructor() {
718
- this._alert = inject(AlertController);
1014
+ this._platform = inject(Platform$1);
1015
+ this._loading = inject(IDEALoadingService);
1016
+ this._message = inject(IDEAMessageService);
1017
+ this._tc = inject(IDEATinCanService);
1018
+ this._api = inject(IDEAAWSAPIService);
1019
+ this._offline = inject(IDEAOfflineService);
719
1020
  this._translate = inject(IDEATranslationsService);
720
1021
  /**
721
- * The contacts to manage.
1022
+ * The team from which we want to load the resources. Default: try to guess current team.
722
1023
  */
723
- this.contacts = new Contacts();
1024
+ this.team = null;
724
1025
  /**
725
- * If true, show the field `name`.
1026
+ * The path to the online API resource, as an array. Don't include the team. E.g. `['entities', entityId]`.
726
1027
  */
727
- this.showName = false;
1028
+ this.pathResource = [];
728
1029
  /**
729
- * Whether the fields are editable or disabled.
1030
+ * The array in which we want to add/remove attachments.
730
1031
  */
731
- this.editMode = true;
1032
+ this.attachments = null;
1033
+ /**
1034
+ * Regulate the mode (view/edit).
1035
+ */
1036
+ this.editMode = false;
1037
+ /**
1038
+ * Show errors as reported from the parent component.
1039
+ */
1040
+ this.errors = new Set();
1041
+ /**
1042
+ * The lines attribute of the item.
1043
+ */
1044
+ this.lines = 'none';
1045
+ /**
1046
+ * Stack of errors from the last upload.
1047
+ */
1048
+ this.uploadErrors = [];
732
1049
  }
733
- sendEmail() {
734
- if (!this.contacts.email)
735
- return;
736
- const url = `mailto:${this.contacts.email}`;
737
- this.preExternalAction(this.contacts.email, () => window.open(url, '_system'));
1050
+ ngOnInit() {
1051
+ // if the team isn't specified, try to guess it in the usual IDEA's paths
1052
+ this.team = this.team || this._tc.get('membership').teamId || this._tc.get('teamId');
1053
+ this.requestURL = `teams/${this.team}/`;
1054
+ if (this.pathResource && this.pathResource.length)
1055
+ this.requestURL = this.requestURL.concat(this.pathResource.filter(x => x).join('/'));
1056
+ }
1057
+ isCapacitor() {
1058
+ return this._platform.is('capacitor');
1059
+ }
1060
+ hasFieldAnError(field) {
1061
+ return this.errors.has(field);
1062
+ }
1063
+ browseFiles() {
1064
+ document.getElementById('attachmentPicker').click();
1065
+ }
1066
+ addAttachmentFromFile(ev) {
1067
+ this.uploadErrors = new Array();
1068
+ const files = ev.target ? ev.target.files : {};
1069
+ for (let i = 0; i < files.length; i++) {
1070
+ const file = files.item(i);
1071
+ const fullName = file.name.split('.');
1072
+ const format = fullName.pop();
1073
+ const name = fullName.join('.');
1074
+ this.addAttachment(name, format, file);
1075
+ }
738
1076
  }
739
- call() {
740
- if (!this.contacts.phone)
1077
+ async takePictureAndAttach(ev) {
1078
+ if (ev)
1079
+ ev.stopPropagation();
1080
+ if (!this._platform.is('capacitor') || !Camera)
741
1081
  return;
742
- const url = `tel:${this.contacts.phone}`;
743
- this.preExternalAction(this.contacts.phone, () => window.open(url, '_system'));
1082
+ const image = await Camera.getPhoto({
1083
+ quality: 90,
1084
+ allowEditing: false,
1085
+ source: CameraSource.Camera,
1086
+ resultType: CameraResultType.Base64
1087
+ });
1088
+ const filename = new Date().toISOString();
1089
+ const content = this.base64toBlob(image.base64String, 'image/jpeg');
1090
+ this.addAttachment(filename, image.format, content);
1091
+ }
1092
+ base64toBlob(base64str, type) {
1093
+ const binary = atob(base64str);
1094
+ const array = [];
1095
+ for (let i = 0; i < binary.length; i++)
1096
+ array.push(binary.charCodeAt(i));
1097
+ return new Blob([new Uint8Array(array)], { type });
1098
+ }
1099
+ async addAttachment(name, format, content) {
1100
+ if (format === FileFormatTypes.HEIC) {
1101
+ format = FileFormatTypes.JPEG;
1102
+ content = await heic2any({ blob: content, toType: 'image/jpeg' });
1103
+ }
1104
+ const attachment = new Attachment({ name, format });
1105
+ this.attachments.push(attachment);
1106
+ try {
1107
+ const signedURL = await this._api.patchResource(this.requestURL, {
1108
+ body: { action: 'ATTACHMENTS_PUT', attachmentId: attachment.attachmentId }
1109
+ });
1110
+ await this._api.rawRequest().put(signedURL.url, content).toPromise();
1111
+ attachment.attachmentId = signedURL.id;
1112
+ }
1113
+ catch (error) {
1114
+ this.uploadErrors.push(name);
1115
+ this.removeAttachment(attachment);
1116
+ this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT');
1117
+ }
744
1118
  }
745
- preExternalAction(message, cb) {
746
- const header = this._translate._('COMMON.DO_YOU_WANT_TO_PROCEED');
747
- const buttons = [
748
- { text: this._translate._('COMMON.CANCEL') },
749
- { text: this._translate._('COMMON.CONFIRM'), handler: () => cb() }
750
- ];
751
- const cssClass = 'selectableAlertMessage';
752
- this._alert.create({ header, message, buttons, cssClass }).then(alert => alert.present());
1119
+ removeAttachment(attachment) {
1120
+ const index = this.attachments.indexOf(attachment);
1121
+ if (index !== -1)
1122
+ this.attachments.splice(index, 1);
753
1123
  }
754
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAContactsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
755
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAContactsComponent, isStandalone: true, selector: "idea-contacts", inputs: { contacts: "contacts", showName: "showName", editMode: "editMode", lines: "lines", color: "color" }, ngImport: i0, template: `
756
- <div class="contacts">
757
- @if (showName) {
758
- <ion-item [lines]="lines" [color]="color">
759
- <ion-input
760
- autocomplete="new"
761
- labelPlacement="stacked"
762
- [label]="'IDEA_UNCOMMON.CONTACTS.NAME' | translate"
763
- [disabled]="!editMode"
764
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
765
- [title]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
766
- [(ngModel)]="contacts.name"
767
- />
768
- </ion-item>
769
- }
770
- <ion-item [lines]="lines" [color]="color">
771
- <ion-input
772
- autocomplete="new"
773
- labelPlacement="stacked"
774
- [label]="'IDEA_UNCOMMON.CONTACTS.PHONE' | translate"
775
- [disabled]="!editMode"
776
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
777
- [title]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
778
- [(ngModel)]="contacts.phone"
779
- />
780
- @if (!editMode) {
781
- <ion-button
782
- slot="end"
783
- fill="clear"
784
- color="dark"
785
- class="marginTop"
786
- [title]="'IDEA_UNCOMMON.CONTACTS.CALL' | translate"
787
- (click)="call()"
788
- >
789
- <ion-icon name="call" slot="icon-only" />
790
- </ion-button>
1124
+ async openAttachment(attachment) {
1125
+ try {
1126
+ await this._loading.show();
1127
+ const { url } = await this._api.patchResource(this.requestURL, {
1128
+ body: { action: 'ATTACHMENTS_GET', attachmentId: attachment.attachmentId }
1129
+ });
1130
+ await Browser.open({ url });
1131
+ }
1132
+ catch (error) {
1133
+ this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_OPENING_ATTACHMENT');
1134
+ }
1135
+ finally {
1136
+ this._loading.hide();
791
1137
  }
792
- </ion-item>
793
- <ion-item [lines]="lines" [color]="color">
794
- <ion-input
795
- autocomplete="new"
796
- labelPlacement="stacked"
797
- [label]="'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate"
798
- [disabled]="!editMode"
799
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
800
- [title]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
801
- [(ngModel)]="contacts.email"
802
- />
803
- @if (!editMode) {
804
- <ion-button
805
- slot="end"
806
- fill="clear"
807
- color="dark"
808
- class="marginTop"
809
- [title]="'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate"
810
- (click)="sendEmail()"
811
- >
812
- <ion-icon name="mail" slot="icon-only" />
1138
+ }
1139
+ getFormatIcon(format) {
1140
+ switch (format) {
1141
+ case FileFormatTypes.JPG:
1142
+ case FileFormatTypes.JPEG:
1143
+ case FileFormatTypes.PNG:
1144
+ return 'image';
1145
+ case FileFormatTypes.PDF:
1146
+ return 'document';
1147
+ default:
1148
+ return 'help';
1149
+ }
1150
+ }
1151
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOldAttachmentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1152
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAOldAttachmentsComponent, isStandalone: true, selector: "idea-old-attachments", inputs: { team: "team", pathResource: "pathResource", attachments: "attachments", editMode: "editMode", errors: "errors", lines: "lines" }, ngImport: i0, template: `
1153
+ @for (att of attachments; track att; let index = $index; let odd = $odd) {
1154
+ <ion-item
1155
+ class="attachments"
1156
+ [class.fieldHasError]="hasFieldAnError('attachments[' + index + '].attachmentId')"
1157
+ [class.odd]="odd"
1158
+ [lines]="lines"
1159
+ >
1160
+ @if (att.attachmentId && _offline.isOnline()) {
1161
+ <ion-button fill="clear" size="small" slot="start" (click)="openAttachment(att)">
1162
+ <ion-icon name="open-outline" slot="icon-only" size="small" />
813
1163
  </ion-button>
814
1164
  }
815
- </ion-item>
816
- </div>
817
- `, isInline: true, styles: [".marginTop{margin-top:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "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", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
818
- }
819
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAContactsComponent, decorators: [{
820
- type: Component,
821
- args: [{ selector: 'idea-contacts', standalone: true, imports: [CommonModule, FormsModule, IDEATranslatePipe, IonInput, IonIcon, IonButton, IonItem], template: `
822
- <div class="contacts">
823
- @if (showName) {
824
- <ion-item [lines]="lines" [color]="color">
1165
+ @if (!editMode && att.attachmentId) {
1166
+ <ion-icon slot="start" color="medium" [name]="getFormatIcon(att.format)" [title]="att.format" />
1167
+ }
1168
+ @if (editMode && att.attachmentId) {
825
1169
  <ion-input
826
- autocomplete="new"
827
- labelPlacement="stacked"
828
- [label]="'IDEA_UNCOMMON.CONTACTS.NAME' | translate"
829
- [disabled]="!editMode"
830
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
831
- [title]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
832
- [(ngModel)]="contacts.name"
1170
+ [(ngModel)]="att.name"
1171
+ (ionBlur)="$event.target.value = $event.target.value || _translate._('IDEA_TEAMS.ATTACHMENTS.NO_NAME')"
833
1172
  />
834
- </ion-item>
835
- }
836
- <ion-item [lines]="lines" [color]="color">
837
- <ion-input
838
- autocomplete="new"
839
- labelPlacement="stacked"
840
- [label]="'IDEA_UNCOMMON.CONTACTS.PHONE' | translate"
841
- [disabled]="!editMode"
842
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
843
- [title]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
844
- [(ngModel)]="contacts.phone"
845
- />
846
- @if (!editMode) {
847
- <ion-button
848
- slot="end"
849
- fill="clear"
850
- color="dark"
851
- class="marginTop"
852
- [title]="'IDEA_UNCOMMON.CONTACTS.CALL' | translate"
853
- (click)="call()"
854
- >
855
- <ion-icon name="call" slot="icon-only" />
856
- </ion-button>
857
1173
  }
858
- </ion-item>
859
- <ion-item [lines]="lines" [color]="color">
860
- <ion-input
861
- autocomplete="new"
862
- labelPlacement="stacked"
863
- [label]="'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate"
864
- [disabled]="!editMode"
865
- [placeholder]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
866
- [title]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
867
- [(ngModel)]="contacts.email"
868
- />
1174
+ @if (editMode && !att.attachmentId) {
1175
+ <ion-label class="loadingWarning">
1176
+ {{ 'IDEA_TEAMS.ATTACHMENTS.UPLOADING_ATTACHMENT_WARNING' | translate }}
1177
+ </ion-label>
1178
+ }
869
1179
  @if (!editMode) {
1180
+ <ion-label>{{ att.name }}.{{ att.format }}</ion-label>
1181
+ }
1182
+ @if (!att.attachmentId) {
1183
+ <ion-spinner slot="end" [title]="'IDEA_TEAMS.ATTACHMENTS.UPLOADING' | translate" />
1184
+ }
1185
+ @if (editMode) {
870
1186
  <ion-button
871
1187
  slot="end"
1188
+ color="danger"
872
1189
  fill="clear"
873
- color="dark"
874
- class="marginTop"
875
- [title]="'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate"
876
- (click)="sendEmail()"
1190
+ [title]="'IDEA_TEAMS.ATTACHMENTS.REMOVE_ATTACHMENT' | translate"
1191
+ (click)="removeAttachment(att)"
877
1192
  >
878
- <ion-icon name="mail" slot="icon-only" />
1193
+ <ion-icon name="remove" slot="icon-only" />
879
1194
  </ion-button>
880
1195
  }
881
1196
  </ion-item>
882
- </div>
883
- `, styles: [".marginTop{margin-top:14px}\n"] }]
884
- }], propDecorators: { contacts: [{
885
- type: Input
886
- }], showName: [{
887
- type: Input
888
- }], editMode: [{
889
- type: Input
890
- }], lines: [{
891
- type: Input
892
- }], color: [{
893
- type: Input
894
- }] } });
895
-
896
- /**
897
- * A random sunday, used as reference to calculate the week days.
898
- */
899
- const SUNDAY = 259200000;
900
- class IDEACalendarPickerComponent {
901
- constructor() {
902
- this._modal = inject(ModalController);
903
- this._alert = inject(AlertController);
904
- this._translate = inject(IDEATranslationsService);
905
- this.timePicker = false;
906
- this.manualTimePicker = false;
907
- this.hideClearButton = false;
908
1197
  }
909
- ngOnInit() {
910
- this.today = new Date();
911
- this.refDate = this.inputDate ? new Date(this.inputDate) : new Date(this.today);
912
- this.selectedDate = new Date(this.refDate);
913
- this.hour = 12; // to endure timezones
914
- this.minute = 0;
915
- if (this.timePicker || this.manualTimePicker) {
916
- this.hour = this.selectedDate.getHours();
917
- if (this.manualTimePicker)
918
- this.minute = this.selectedDate.getMinutes();
919
- else {
920
- // round the minutes a multiple of 5
921
- this.minute = Math.ceil(this.selectedDate.getMinutes() / 5) * 5;
922
- this.selectedDate.setMinutes(this.minute);
923
- }
924
- }
925
- this.buildCalendarGrid(this.refDate);
926
- this.hours = Array.from(Array(24).keys()).map(i => '0'.concat(i.toString()).slice(-2));
927
- this.minutes = Array.from(Array(12).keys()).map(i => '0'.concat((i * 5).toString()).slice(-2));
928
- // build the weekdays based on the current locale
929
- this.weekDays = new Array();
930
- const refDateForWeekDays = new Date(SUNDAY);
931
- refDateForWeekDays.setDate(refDateForWeekDays.getDate() - this.getLocalisedDay(refDateForWeekDays));
932
- for (let i = 0; i < 7; i++) {
933
- this.weekDays.push(refDateForWeekDays.toLocaleDateString(this._translate.getCurrentLang(), { weekday: 'short' }));
934
- refDateForWeekDays.setDate(refDateForWeekDays.getDate() + 1);
935
- }
936
- if (this.min)
937
- this.dateMin = new Date(this.min);
938
- if (this.max)
939
- this.dateMax = new Date(this.max);
1198
+ <!----->
1199
+ @if (!editMode && !attachments?.length) {
1200
+ <ion-item lines="none" class="noAttachments">
1201
+ <ion-label>
1202
+ {{ 'IDEA_TEAMS.ATTACHMENTS.NO_ATTACHMENT' | translate }}
1203
+ </ion-label>
1204
+ </ion-item>
940
1205
  }
941
- getLocalisedDay(date) {
942
- let n = date.getDay();
943
- switch (this._translate.getCurrentLang()) {
944
- case 'it':
945
- n = --n < 0 ? 6 : n;
946
- break;
1206
+ <!----->
1207
+ @if (editMode) {
1208
+ <div>
1209
+ @for (err of uploadErrors; track err) {
1210
+ <ion-item class="attachments" [lines]="lines">
1211
+ <ion-icon name="alert-circle" slot="start" color="danger" />
1212
+ <ion-label color="danger">
1213
+ {{ err }}
1214
+ <p>{{ 'IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT' | translate }}</p>
1215
+ </ion-label>
1216
+ <ion-button
1217
+ slot="end"
1218
+ color="danger"
1219
+ fill="clear"
1220
+ [title]="'IDEA_TEAMS.ATTACHMENTS.HIDE_ERROR' | translate"
1221
+ (click)="uploadErrors.splice(uploadErrors.indexOf(err), 1)"
1222
+ >
1223
+ <ion-icon name="close" slot="icon-only" />
1224
+ </ion-button>
1225
+ </ion-item>
947
1226
  }
948
- return n;
1227
+ </div>
949
1228
  }
950
- buildCalendarGrid(refDate) {
951
- // find the first day in the month: the important data here is the day of the week
952
- const firstDateOfMonth = new Date(refDate.getFullYear(), refDate.getMonth(), 1);
953
- // the following flag is used to divide the logic so I can fill the calendar
954
- // also with the dates from the previous month, until there's space in the grid
955
- let haventFoundFirstDay = true;
956
- // index used to build the dates of the month, starting from the first one
957
- let index = 1;
958
- this.calendarGrid = new Array();
959
- for (let i = 0; i < 6; i++) {
960
- this.calendarGrid[i] = new Array();
961
- for (let j = 0; j < 7; j++) {
962
- if (haventFoundFirstDay) {
963
- if (this.getLocalisedDay(firstDateOfMonth) === j) {
964
- // note: considers Sunday
965
- haventFoundFirstDay = false;
966
- this.calendarGrid[i][j] = new Date(firstDateOfMonth);
967
- // now the I've found the first date of the month I can fill the calendar
968
- // the dates fromt the previous month, until there's space in the grid
969
- for (let y = this.getLocalisedDay(firstDateOfMonth); y >= 0; y--) {
970
- const d = new Date(firstDateOfMonth);
971
- d.setDate(firstDateOfMonth.getDate() - y);
972
- this.calendarGrid[i][this.getLocalisedDay(firstDateOfMonth) - y] = d;
973
- }
974
- }
975
- }
976
- else {
977
- // fill the following dates until there's space in the grid
978
- const d = new Date(firstDateOfMonth);
979
- d.setDate(firstDateOfMonth.getDate() + index++);
980
- this.calendarGrid[i][j] = d;
981
- }
982
- }
1229
+ <!----->
1230
+ @if (editMode) {
1231
+ <ion-item lines="none" class="selectable" (click)="browseFiles()">
1232
+ <ion-label color="medium">{{ 'IDEA_TEAMS.ATTACHMENTS.TAP_TO_ADD_ATTACHMENT' | translate }}</ion-label>
1233
+ <input
1234
+ id="attachmentPicker"
1235
+ type="file"
1236
+ multiple
1237
+ style="display: none"
1238
+ (change)="addAttachmentFromFile($event)"
1239
+ />
1240
+ @if (editMode) {
1241
+ <ion-icon slot="end" name="caret-down" class="selectIcon" />
983
1242
  }
984
- }
985
- addYears(offset) {
986
- this.refDate.setFullYear(this.refDate.getFullYear() + offset);
987
- this.buildCalendarGrid(this.refDate);
988
- }
989
- setYear(year) {
990
- if (!year)
991
- return;
992
- this.refDate.setFullYear(Number(year));
993
- this.buildCalendarGrid(this.refDate);
994
- }
995
- addMonths(offset) {
996
- this.refDate.setMonth(this.refDate.getMonth() + offset);
997
- this.buildCalendarGrid(this.refDate);
998
- }
999
- async showMonths() {
1000
- const buttons = [];
1001
- const inputs = [];
1002
- const month = new Date(0);
1003
- for (let i = 1; i <= 12; i++) {
1004
- inputs.push({
1005
- type: 'radio',
1006
- label: month.toLocaleDateString(this._translate.getCurrentLang(), { month: 'long' }),
1007
- value: i.toString(),
1008
- checked: i === this.refDate.getMonth() + 1
1009
- });
1010
- month.setMonth(month.getMonth() + 1);
1243
+ @if (editMode && isCapacitor()) {
1244
+ <ion-button slot="end" fill="clear" color="dark" (click)="takePictureAndAttach($event)">
1245
+ <ion-icon slot="icon-only" name="camera" />
1246
+ </ion-button>
1011
1247
  }
1012
- buttons.push({ text: this._translate._('COMMON.CANCEL'), role: 'cancel' });
1013
- buttons.push({
1014
- text: this._translate._('COMMON.SELECT'),
1015
- handler: (m) => {
1016
- this.refDate.setMonth(parseInt(m, 10) - 1);
1017
- this.buildCalendarGrid(this.refDate);
1018
- }
1019
- });
1020
- const alert = await this._alert.create({
1021
- header: this._translate._('IDEA_UNCOMMON.CALENDAR.MONTH'),
1022
- buttons,
1023
- inputs
1024
- });
1025
- alert.present();
1026
- }
1027
- isDateSelectable(date) {
1028
- if (this.dateMin && date < this.dateMin)
1029
- return false;
1030
- if (this.dateMax && date > this.dateMax)
1031
- return false;
1032
- return true;
1033
- }
1034
- selectDate(date) {
1035
- this.selectedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), this.hour, this.minute);
1036
- }
1037
- selectHour(hour) {
1038
- this.hour = Number(hour);
1039
- this.selectedDate.setHours(this.hour);
1040
- }
1041
- selectMinute(minute) {
1042
- this.minute = Number(minute);
1043
- this.selectedDate.setMinutes(this.minute);
1044
- }
1045
- isSameDay(a, b) {
1046
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1047
- }
1048
- isSameMonth(a, b) {
1049
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
1248
+ </ion-item>
1050
1249
  }
1051
- isSameHour(hour) {
1052
- return this.selectedDate.getHours() === Number(hour);
1250
+ `, isInline: true, styles: [".attachments ion-button[slot=start],.attachments ion-icon[slot=start]{margin-right:10px}.attachments .loadingWarning{font-style:italic;font-size:.8em;color:var(--ion-color-medium)}.noAttachments{font-style:italic}.selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectable{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: 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: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "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", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
1251
+ }
1252
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOldAttachmentsComponent, decorators: [{
1253
+ type: Component,
1254
+ args: [{ selector: 'idea-old-attachments', imports: [FormsModule, IDEATranslatePipe, IonItem, IonButton, IonIcon, IonInput, IonLabel, IonSpinner], template: `
1255
+ @for (att of attachments; track att; let index = $index; let odd = $odd) {
1256
+ <ion-item
1257
+ class="attachments"
1258
+ [class.fieldHasError]="hasFieldAnError('attachments[' + index + '].attachmentId')"
1259
+ [class.odd]="odd"
1260
+ [lines]="lines"
1261
+ >
1262
+ @if (att.attachmentId && _offline.isOnline()) {
1263
+ <ion-button fill="clear" size="small" slot="start" (click)="openAttachment(att)">
1264
+ <ion-icon name="open-outline" slot="icon-only" size="small" />
1265
+ </ion-button>
1266
+ }
1267
+ @if (!editMode && att.attachmentId) {
1268
+ <ion-icon slot="start" color="medium" [name]="getFormatIcon(att.format)" [title]="att.format" />
1269
+ }
1270
+ @if (editMode && att.attachmentId) {
1271
+ <ion-input
1272
+ [(ngModel)]="att.name"
1273
+ (ionBlur)="$event.target.value = $event.target.value || _translate._('IDEA_TEAMS.ATTACHMENTS.NO_NAME')"
1274
+ />
1275
+ }
1276
+ @if (editMode && !att.attachmentId) {
1277
+ <ion-label class="loadingWarning">
1278
+ {{ 'IDEA_TEAMS.ATTACHMENTS.UPLOADING_ATTACHMENT_WARNING' | translate }}
1279
+ </ion-label>
1280
+ }
1281
+ @if (!editMode) {
1282
+ <ion-label>{{ att.name }}.{{ att.format }}</ion-label>
1283
+ }
1284
+ @if (!att.attachmentId) {
1285
+ <ion-spinner slot="end" [title]="'IDEA_TEAMS.ATTACHMENTS.UPLOADING' | translate" />
1286
+ }
1287
+ @if (editMode) {
1288
+ <ion-button
1289
+ slot="end"
1290
+ color="danger"
1291
+ fill="clear"
1292
+ [title]="'IDEA_TEAMS.ATTACHMENTS.REMOVE_ATTACHMENT' | translate"
1293
+ (click)="removeAttachment(att)"
1294
+ >
1295
+ <ion-icon name="remove" slot="icon-only" />
1296
+ </ion-button>
1297
+ }
1298
+ </ion-item>
1053
1299
  }
1054
- isSameMinute(minute) {
1055
- return this.selectedDate.getMinutes() === Number(minute);
1300
+ <!----->
1301
+ @if (!editMode && !attachments?.length) {
1302
+ <ion-item lines="none" class="noAttachments">
1303
+ <ion-label>
1304
+ {{ 'IDEA_TEAMS.ATTACHMENTS.NO_ATTACHMENT' | translate }}
1305
+ </ion-label>
1306
+ </ion-item>
1056
1307
  }
1057
- save(reset) {
1058
- this._modal.dismiss(reset ? '' : this.selectedDate);
1308
+ <!----->
1309
+ @if (editMode) {
1310
+ <div>
1311
+ @for (err of uploadErrors; track err) {
1312
+ <ion-item class="attachments" [lines]="lines">
1313
+ <ion-icon name="alert-circle" slot="start" color="danger" />
1314
+ <ion-label color="danger">
1315
+ {{ err }}
1316
+ <p>{{ 'IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT' | translate }}</p>
1317
+ </ion-label>
1318
+ <ion-button
1319
+ slot="end"
1320
+ color="danger"
1321
+ fill="clear"
1322
+ [title]="'IDEA_TEAMS.ATTACHMENTS.HIDE_ERROR' | translate"
1323
+ (click)="uploadErrors.splice(uploadErrors.indexOf(err), 1)"
1324
+ >
1325
+ <ion-icon name="close" slot="icon-only" />
1326
+ </ion-button>
1327
+ </ion-item>
1328
+ }
1329
+ </div>
1059
1330
  }
1060
- close() {
1061
- this._modal.dismiss();
1331
+ <!----->
1332
+ @if (editMode) {
1333
+ <ion-item lines="none" class="selectable" (click)="browseFiles()">
1334
+ <ion-label color="medium">{{ 'IDEA_TEAMS.ATTACHMENTS.TAP_TO_ADD_ATTACHMENT' | translate }}</ion-label>
1335
+ <input
1336
+ id="attachmentPicker"
1337
+ type="file"
1338
+ multiple
1339
+ style="display: none"
1340
+ (change)="addAttachmentFromFile($event)"
1341
+ />
1342
+ @if (editMode) {
1343
+ <ion-icon slot="end" name="caret-down" class="selectIcon" />
1344
+ }
1345
+ @if (editMode && isCapacitor()) {
1346
+ <ion-button slot="end" fill="clear" color="dark" (click)="takePictureAndAttach($event)">
1347
+ <ion-icon slot="icon-only" name="camera" />
1348
+ </ion-button>
1349
+ }
1350
+ </ion-item>
1062
1351
  }
1063
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEACalendarPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1064
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEACalendarPickerComponent, isStandalone: true, 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: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "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", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: 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: IonRow, selector: "ion-row" }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }, { kind: "pipe", type: IDEALocalizedDatePipe, name: "dateLocale" }] }); }
1065
- }
1066
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEACalendarPickerComponent, decorators: [{
1067
- type: Component,
1068
- args: [{ selector: 'idea-calendar-picker', standalone: true, imports: [
1069
- CommonModule,
1070
- FormsModule,
1071
- IDEATranslatePipe,
1072
- IDEALocalizedDatePipe,
1073
- IonFooter,
1074
- IonInput,
1075
- IonCol,
1076
- IonRow,
1077
- IonContent,
1078
- IonGrid,
1079
- IonIcon,
1080
- IonButton,
1081
- IonButtons,
1082
- IonTitle,
1083
- IonToolbar,
1084
- IonHeader
1085
- ], 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"] }]
1086
- }], propDecorators: { inputDate: [{
1352
+ `, styles: [".attachments ion-button[slot=start],.attachments ion-icon[slot=start]{margin-right:10px}.attachments .loadingWarning{font-style:italic;font-size:.8em;color:var(--ion-color-medium)}.noAttachments{font-style:italic}.selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectable{cursor:pointer}\n"] }]
1353
+ }], propDecorators: { team: [{
1087
1354
  type: Input
1088
- }], timePicker: [{
1355
+ }], pathResource: [{
1089
1356
  type: Input
1090
- }], manualTimePicker: [{
1357
+ }], attachments: [{
1091
1358
  type: Input
1092
- }], title: [{
1359
+ }], editMode: [{
1093
1360
  type: Input
1094
- }], hideClearButton: [{
1361
+ }], errors: [{
1095
1362
  type: Input
1096
- }], min: [{
1363
+ }], lines: [{
1097
1364
  type: Input
1098
- }], max: [{
1365
+ }] } });
1366
+ /**
1367
+ * The possibile file types (formats).
1368
+ */
1369
+ var FileFormatTypes;
1370
+ (function (FileFormatTypes) {
1371
+ FileFormatTypes["JPG"] = "jpg";
1372
+ FileFormatTypes["JPEG"] = "jpeg";
1373
+ FileFormatTypes["PNG"] = "png";
1374
+ FileFormatTypes["PDF"] = "pdf";
1375
+ FileFormatTypes["HEIC"] = "heic";
1376
+ })(FileFormatTypes || (FileFormatTypes = {}));
1377
+
1378
+ /**
1379
+ * Announcement card that can be set in any interface to show the alert for this project.
1380
+ */
1381
+ class IDEAAnnouncementComponent {
1382
+ constructor() {
1383
+ this._env = inject(IDEAEnvironment);
1384
+ this._tc = inject(IDEATinCanService);
1385
+ this._storage = inject(IDEAStorageService);
1386
+ this.storageKey = (this._env.idea.project || 'app').concat('_LAST_ANNOUNCEMENT');
1387
+ this.color = 'dark';
1388
+ }
1389
+ async ngOnInit() {
1390
+ // standard IDEA: the announcement is read in the Init Guard
1391
+ this.announcement = this._tc.get('idea-announcement');
1392
+ if (this.announcement && this.announcement.content) {
1393
+ const lastRead = await this._storage.get(this.storageKey);
1394
+ if (!lastRead || this.announcement.content !== lastRead)
1395
+ this.htmlContent = mdToHtml(this.announcement.content);
1396
+ }
1397
+ }
1398
+ async markAsRead() {
1399
+ await this._storage.set(this.storageKey, this.announcement.content);
1400
+ this.htmlContent = null;
1401
+ }
1402
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAnnouncementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1403
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAAnnouncementComponent, isStandalone: true, selector: "idea-announcement", inputs: { color: "color" }, ngImport: i0, template: `
1404
+ @if (htmlContent) {
1405
+ <ion-card class="announcementCard" [color]="color">
1406
+ <ion-card-content>
1407
+ <p class="htmlContent" [innerHTML]="htmlContent"></p>
1408
+ <p class="ion-text-right">
1409
+ <ion-button color="primary" (click)="markAsRead()">
1410
+ {{ 'COMMON.GOT_IT' | translate }}
1411
+ </ion-button>
1412
+ </p>
1413
+ </ion-card-content>
1414
+ </ion-card>
1415
+ }
1416
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: 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: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
1417
+ }
1418
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAnnouncementComponent, decorators: [{
1419
+ type: Component,
1420
+ args: [{
1421
+ selector: 'idea-announcement',
1422
+ standalone: true,
1423
+ imports: [CommonModule, IDEATranslatePipe, IonButton, IonCardContent, IonCard],
1424
+ template: `
1425
+ @if (htmlContent) {
1426
+ <ion-card class="announcementCard" [color]="color">
1427
+ <ion-card-content>
1428
+ <p class="htmlContent" [innerHTML]="htmlContent"></p>
1429
+ <p class="ion-text-right">
1430
+ <ion-button color="primary" (click)="markAsRead()">
1431
+ {{ 'COMMON.GOT_IT' | translate }}
1432
+ </ion-button>
1433
+ </p>
1434
+ </ion-card-content>
1435
+ </ion-card>
1436
+ }
1437
+ `
1438
+ }]
1439
+ }], ctorParameters: () => [], propDecorators: { color: [{
1099
1440
  type: Input
1100
1441
  }] } });
1101
1442
 
1102
- class IDEADateTimeComponent {
1443
+ /**
1444
+ * Pick a color from a defined set.
1445
+ */
1446
+ class IDEAColorPickerComponent {
1103
1447
  constructor() {
1104
- this._modal = inject(ModalController);
1105
- this._translate = inject(IDEATranslationsService);
1106
- /**
1107
- * Whether to show the time picker (datetime) or not (date).
1108
- */
1109
- this.timePicker = false;
1110
- /**
1111
- * Whether to show the MANUAL time picker (datetime) or not (date).
1112
- */
1113
- this.manualTimePicker = false;
1114
- /**
1115
- * Whether to use the `epochISOString` format instead of `epochDateTime`.
1116
- */
1117
- this.useISOFormat = false;
1118
1448
  /**
1119
- * If true, the component is disabled.
1449
+ * The pickable colors.
1120
1450
  */
1121
- this.disabled = false;
1451
+ this.colors = COLORS;
1122
1452
  /**
1123
- * If true, the obligatory dot is shown.
1453
+ * On select event.
1124
1454
  */
1125
- this.obligatory = false;
1455
+ this.select = new EventEmitter();
1126
1456
  /**
1127
- * If true, hidew the clear button in the header.
1457
+ * Icon select.
1128
1458
  */
1129
- this.hideClearButton = false;
1130
- this.dateChange = new EventEmitter();
1131
1459
  this.iconSelect = new EventEmitter();
1132
- this.isOpening = false;
1133
- }
1134
- ngOnInit() {
1135
- this.langChangeSubscription = this._translate.onLangChange.subscribe(() => {
1136
- this.valueToDisplay = this.getValueToDisplay(this.date);
1137
- });
1138
- }
1139
- ngOnDestroy() {
1140
- if (this.langChangeSubscription)
1141
- this.langChangeSubscription.unsubscribe();
1142
- }
1143
- ngOnChanges(changes) {
1144
- if (changes.date || changes.timePicker)
1145
- this.valueToDisplay = this.getValueToDisplay(this.date);
1460
+ this._popover = inject(PopoverController);
1146
1461
  }
1147
- async openCalendarPicker() {
1148
- if (this.disabled || this.isOpening)
1462
+ async openPalette(event) {
1463
+ if (this.disabled)
1149
1464
  return;
1150
- this.isOpening = true;
1151
- const modal = await this._modal.create({
1152
- component: IDEACalendarPickerComponent,
1153
- componentProps: {
1154
- inputDate: this.date,
1155
- title: this.label,
1156
- timePicker: this.timePicker,
1157
- manualTimePicker: this.manualTimePicker,
1158
- hideClearButton: this.hideClearButton,
1159
- min: this.min,
1160
- max: this.max
1161
- }
1162
- });
1163
- modal.onDidDismiss().then(({ data }) => {
1164
- if (data !== undefined && data !== null) {
1165
- if (data === '')
1166
- this.doSelect(null);
1167
- else
1168
- this.doSelect(data);
1169
- }
1465
+ const componentProps = { colors: this.colors, current: this.current };
1466
+ const popover = await this._popover.create({ component: ColorsPaletteComponent, componentProps, event });
1467
+ popover.onDidDismiss().then(res => {
1468
+ if (res && res.data)
1469
+ this.select.emit(res.data);
1170
1470
  });
1171
- modal.present();
1172
- this.isOpening = false;
1173
- }
1174
- getValueToDisplay(date) {
1175
- return !date
1176
- ? ''
1177
- : this._translate.formatDate(date, this.timePicker || this.manualTimePicker ? 'd MMM yyyy, HH:mm' : 'mediumDate');
1178
- }
1179
- doSelect(date) {
1180
- this.dateChange.emit(date ? (this.useISOFormat ? date.toISOString() : date.valueOf()) : null);
1471
+ popover.present();
1181
1472
  }
1182
1473
  doIconSelect(event) {
1183
1474
  if (event)
1184
1475
  event.stopPropagation();
1185
- this.iconSelect.emit();
1476
+ this.iconSelect.emit(event);
1186
1477
  }
1187
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADateTimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1188
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEADateTimeComponent, isStandalone: true, 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: `
1478
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1479
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAColorPickerComponent, isStandalone: true, 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: `
1189
1480
  <ion-item
1190
- class="dateTimeItem"
1481
+ class="colorPickerItem"
1191
1482
  [color]="color"
1192
1483
  [lines]="lines"
1193
- [title]="placeholder || label || ''"
1194
1484
  [button]="!disabled"
1195
- [disabled]="isOpening"
1485
+ [title]="placeholder || ''"
1196
1486
  [class.withLabel]="label"
1197
- (click)="openCalendarPicker()"
1487
+ (click)="openPalette($event)"
1198
1488
  >
1199
1489
  @if (icon) {
1200
- <ion-button
1201
- fill="clear"
1202
- slot="start"
1203
- [color]="iconColor"
1204
- [class.marginTop]="label"
1205
- (click)="doIconSelect($event)"
1206
- >
1207
- <ion-icon [icon]="icon" slot="icon-only" />
1490
+ <ion-button fill="clear" slot="start" [color]="iconColor" (click)="doIconSelect($event)">
1491
+ <ion-icon [name]="icon" slot="icon-only" />
1208
1492
  </ion-button>
1209
1493
  }
1210
1494
  @if (label) {
1211
- <ion-label position="stacked" [class.selectable]="!disabled">
1495
+ <ion-label [class.selectable]="!disabled">
1212
1496
  {{ label }}
1213
1497
  @if (obligatory && !disabled) {
1214
1498
  <ion-text class="obligatoryDot" />
1215
1499
  }
1216
1500
  </ion-label>
1217
- }
1218
- <ion-label class="value" [class.selectable]="!disabled">
1219
- @if (!valueToDisplay && !disabled) {
1220
- <ion-text class="placeholder" [class.selectable]="!disabled">
1221
- {{ placeholder }}
1222
- </ion-text>
1223
- }
1224
- {{ valueToDisplay }}
1225
- </ion-label>
1501
+ }
1502
+ <ion-avatar slot="end" class="colorCircle" [style.background-color]="current" />
1226
1503
  @if (!disabled) {
1227
- <ion-icon slot="end" icon="caret-down" class="selectIcon" [class.selectable]="!disabled" />
1504
+ <ion-icon slot="end" name="caret-down" class="selectIcon" [class.selectable]="!disabled" />
1228
1505
  }
1229
1506
  </ion-item>
1230
- `, isInline: true, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }] }); }
1507
+ `, isInline: true, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonAvatar, selector: "ion-avatar" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }] }); }
1231
1508
  }
1232
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADateTimeComponent, decorators: [{
1509
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAColorPickerComponent, decorators: [{
1233
1510
  type: Component,
1234
- args: [{ selector: 'idea-date-time', standalone: true, imports: [CommonModule, IonText, IonLabel, IonIcon, IonButton, IonItem], template: `
1511
+ args: [{ selector: 'idea-color-picker', standalone: true, imports: [CommonModule, IonAvatar, IonText, IonLabel, IonIcon, IonButton, IonItem], template: `
1235
1512
  <ion-item
1236
- class="dateTimeItem"
1513
+ class="colorPickerItem"
1237
1514
  [color]="color"
1238
1515
  [lines]="lines"
1239
- [title]="placeholder || label || ''"
1240
1516
  [button]="!disabled"
1241
- [disabled]="isOpening"
1517
+ [title]="placeholder || ''"
1242
1518
  [class.withLabel]="label"
1243
- (click)="openCalendarPicker()"
1519
+ (click)="openPalette($event)"
1244
1520
  >
1245
1521
  @if (icon) {
1246
- <ion-button
1247
- fill="clear"
1248
- slot="start"
1249
- [color]="iconColor"
1250
- [class.marginTop]="label"
1251
- (click)="doIconSelect($event)"
1252
- >
1253
- <ion-icon [icon]="icon" slot="icon-only" />
1522
+ <ion-button fill="clear" slot="start" [color]="iconColor" (click)="doIconSelect($event)">
1523
+ <ion-icon [name]="icon" slot="icon-only" />
1254
1524
  </ion-button>
1255
1525
  }
1256
1526
  @if (label) {
1257
- <ion-label position="stacked" [class.selectable]="!disabled">
1527
+ <ion-label [class.selectable]="!disabled">
1258
1528
  {{ label }}
1259
1529
  @if (obligatory && !disabled) {
1260
1530
  <ion-text class="obligatoryDot" />
1261
1531
  }
1262
1532
  </ion-label>
1263
1533
  }
1264
- <ion-label class="value" [class.selectable]="!disabled">
1265
- @if (!valueToDisplay && !disabled) {
1266
- <ion-text class="placeholder" [class.selectable]="!disabled">
1267
- {{ placeholder }}
1268
- </ion-text>
1269
- }
1270
- {{ valueToDisplay }}
1271
- </ion-label>
1534
+ <ion-avatar slot="end" class="colorCircle" [style.background-color]="current" />
1272
1535
  @if (!disabled) {
1273
- <ion-icon slot="end" icon="caret-down" class="selectIcon" [class.selectable]="!disabled" />
1536
+ <ion-icon slot="end" name="caret-down" class="selectIcon" [class.selectable]="!disabled" />
1274
1537
  }
1275
1538
  </ion-item>
1276
- `, 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"] }]
1277
- }], propDecorators: { date: [{
1278
- type: Input
1279
- }], timePicker: [{
1280
- type: Input
1281
- }], manualTimePicker: [{
1539
+ `, 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"] }]
1540
+ }], propDecorators: { colors: [{
1282
1541
  type: Input
1283
- }], useISOFormat: [{
1542
+ }], current: [{
1284
1543
  type: Input
1285
1544
  }], label: [{
1286
1545
  type: Input
1546
+ }], placeholder: [{
1547
+ type: Input
1287
1548
  }], icon: [{
1288
1549
  type: Input
1289
1550
  }], iconColor: [{
1290
1551
  type: Input
1291
- }], lines: [{
1292
- type: Input
1293
- }], color: [{
1294
- type: Input
1295
- }], placeholder: [{
1296
- type: Input
1297
1552
  }], disabled: [{
1298
1553
  type: Input
1299
1554
  }], obligatory: [{
1300
1555
  type: Input
1301
- }], hideClearButton: [{
1302
- type: Input
1303
- }], min: [{
1556
+ }], lines: [{
1304
1557
  type: Input
1305
- }], max: [{
1558
+ }], color: [{
1306
1559
  type: Input
1307
- }], dateChange: [{
1560
+ }], select: [{
1308
1561
  type: Output
1309
1562
  }], iconSelect: [{
1310
1563
  type: Output
1311
1564
  }] } });
1565
+ /**
1566
+ * Component to open in a popover for displaying the colors available.
1567
+ */
1568
+ class ColorsPaletteComponent {
1569
+ constructor() {
1570
+ this._popover = inject(PopoverController);
1571
+ }
1572
+ pick(color) {
1573
+ this._popover.dismiss(color);
1574
+ }
1575
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ColorsPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1576
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: ColorsPaletteComponent, isStandalone: true, selector: "idea-colors-palette", inputs: { colors: "colors", current: "current" }, ngImport: i0, template: `
1577
+ <ion-content>
1578
+ <ion-grid>
1579
+ <ion-row>
1580
+ @for (c of colors; track c) {
1581
+ <ion-col [size]="2">
1582
+ <ion-avatar
1583
+ class="colorCircle tappable"
1584
+ [style.background-color]="c.hex"
1585
+ [title]="c.name || c.hex"
1586
+ (click)="pick(c.hex)"
1587
+ >
1588
+ @if (c.hex === current) {
1589
+ <ion-icon name="checkmark" />
1590
+ }
1591
+ </ion-avatar>
1592
+ </ion-col>
1593
+ }
1594
+ </ion-row>
1595
+ </ion-grid>
1596
+ </ion-content>
1597
+ `, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonRow, selector: "ion-row" }, { kind: "component", type: 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: IonAvatar, selector: "ion-avatar" }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
1598
+ }
1599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ColorsPaletteComponent, decorators: [{
1600
+ type: Component,
1601
+ args: [{ selector: 'idea-colors-palette', standalone: true, imports: [CommonModule, IonContent, IonGrid, IonRow, IonCol, IonAvatar, IonIcon], template: `
1602
+ <ion-content>
1603
+ <ion-grid>
1604
+ <ion-row>
1605
+ @for (c of colors; track c) {
1606
+ <ion-col [size]="2">
1607
+ <ion-avatar
1608
+ class="colorCircle tappable"
1609
+ [style.background-color]="c.hex"
1610
+ [title]="c.name || c.hex"
1611
+ (click)="pick(c.hex)"
1612
+ >
1613
+ @if (c.hex === current) {
1614
+ <ion-icon name="checkmark" />
1615
+ }
1616
+ </ion-avatar>
1617
+ </ion-col>
1618
+ }
1619
+ </ion-row>
1620
+ </ion-grid>
1621
+ </ion-content>
1622
+ `, 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"] }]
1623
+ }], propDecorators: { colors: [{
1624
+ type: Input
1625
+ }], current: [{
1626
+ type: Input
1627
+ }] } });
1312
1628
 
1313
- class IDEADurationComponent {
1629
+ class IDEAContactsComponent {
1314
1630
  constructor() {
1631
+ this._alert = inject(AlertController);
1632
+ this._translate = inject(IDEATranslationsService);
1315
1633
  /**
1316
- * On change event. It emits a number of seconds representing the duration.
1634
+ * The contacts to manage.
1317
1635
  */
1318
- this.set = new EventEmitter();
1319
- this.default = this.hours = this.minutes = this.seconds = 0;
1636
+ this.contacts = new Contacts();
1637
+ /**
1638
+ * If true, show the field `name`.
1639
+ */
1640
+ this.showName = false;
1641
+ /**
1642
+ * Whether the fields are editable or disabled.
1643
+ */
1644
+ this.editMode = true;
1320
1645
  }
1321
- ngOnChanges(changes) {
1322
- // assign a default value to the duration
1323
- if (changes['default'] && changes['default'].isFirstChange()) {
1324
- const refDate = new Date(0);
1325
- refDate.setHours(0, 0, 0);
1326
- refDate.setSeconds(refDate.getSeconds() + this.default);
1327
- this.hours = refDate.getHours();
1328
- this.minutes = refDate.getMinutes();
1329
- this.seconds = this.hideSeconds ? 0 : refDate.getSeconds();
1330
- }
1646
+ sendEmail() {
1647
+ if (!this.contacts.email)
1648
+ return;
1649
+ const url = `mailto:${this.contacts.email}`;
1650
+ this.preExternalAction(this.contacts.email, () => window.open(url, '_system'));
1331
1651
  }
1332
- /**
1333
- * When one of the parts changes, emit the new duration.
1334
- */
1335
- setDuration(type) {
1336
- switch (type) {
1337
- case 'hours':
1338
- const hh = Number(this.hours) || 0;
1339
- this.hours = hh > 23 ? 23 : hh < 0 ? 0 : hh;
1340
- break;
1341
- case 'minutes':
1342
- const mm = Number(this.minutes) || 0;
1343
- this.minutes = mm > 59 ? 59 : mm < 0 ? 0 : mm;
1344
- break;
1345
- case 'seconds':
1346
- const ss = Number(this.seconds) || 0;
1347
- if (this.hideSeconds)
1348
- this.seconds = 0;
1349
- else
1350
- this.seconds = ss > 59 ? 59 : ss < 0 ? 0 : ss;
1351
- break;
1352
- }
1353
- this.set.emit(this.hours * 3600 + this.minutes * 60 + this.seconds);
1652
+ call() {
1653
+ if (!this.contacts.phone)
1654
+ return;
1655
+ const url = `tel:${this.contacts.phone}`;
1656
+ this.preExternalAction(this.contacts.phone, () => window.open(url, '_system'));
1354
1657
  }
1355
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1356
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEADurationComponent, isStandalone: true, 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: `
1357
- <ion-item class="durationItem" [lines]="lines" [color]="color" [title]="title || label">
1358
- @if (icon) {
1359
- <ion-icon slot="start" [name]="icon" />
1360
- }
1361
- @if (!icon && label) {
1362
- <ion-label position="stacked">
1363
- {{ label }}
1364
- @if (obligatory && !disabled) {
1365
- <ion-text class="obligatoryDot" />
1366
- }
1367
- </ion-label>
1368
- }
1369
- <ion-label class="value">
1370
- <ion-input
1371
- type="number"
1372
- min="0"
1373
- max="23"
1374
- inputmode="numeric"
1375
- [disabled]="disabled"
1376
- [(ngModel)]="hours"
1377
- (ionChange)="setDuration('hours')"
1378
- />
1379
- <ion-text>{{
1380
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()
1381
- }}</ion-text>
1382
- <ion-input
1383
- type="number"
1384
- min="0"
1385
- max="59"
1386
- inputmode="numeric"
1387
- [disabled]="disabled"
1388
- [(ngModel)]="minutes"
1389
- (ionChange)="setDuration('minutes')"
1390
- />
1391
- <ion-text>{{
1392
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()
1393
- }}</ion-text>
1394
- @if (!hideSeconds) {
1658
+ preExternalAction(message, cb) {
1659
+ const header = this._translate._('COMMON.DO_YOU_WANT_TO_PROCEED');
1660
+ const buttons = [
1661
+ { text: this._translate._('COMMON.CANCEL') },
1662
+ { text: this._translate._('COMMON.CONFIRM'), handler: () => cb() }
1663
+ ];
1664
+ const cssClass = 'selectableAlertMessage';
1665
+ this._alert.create({ header, message, buttons, cssClass }).then(alert => alert.present());
1666
+ }
1667
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAContactsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1668
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAContactsComponent, isStandalone: true, selector: "idea-contacts", inputs: { contacts: "contacts", showName: "showName", editMode: "editMode", lines: "lines", color: "color" }, ngImport: i0, template: `
1669
+ <div class="contacts">
1670
+ @if (showName) {
1671
+ <ion-item [lines]="lines" [color]="color">
1395
1672
  <ion-input
1396
- type="number"
1397
- min="0"
1398
- max="59"
1399
- inputmode="numeric"
1400
- [disabled]="disabled"
1401
- [(ngModel)]="seconds"
1402
- (ionChange)="setDuration('seconds')"
1673
+ autocomplete="new"
1674
+ labelPlacement="stacked"
1675
+ [label]="'IDEA_UNCOMMON.CONTACTS.NAME' | translate"
1676
+ [disabled]="!editMode"
1677
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
1678
+ [title]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
1679
+ [(ngModel)]="contacts.name"
1403
1680
  />
1681
+ </ion-item>
1682
+ }
1683
+ <ion-item [lines]="lines" [color]="color">
1684
+ <ion-input
1685
+ autocomplete="new"
1686
+ labelPlacement="stacked"
1687
+ [label]="'IDEA_UNCOMMON.CONTACTS.PHONE' | translate"
1688
+ [disabled]="!editMode"
1689
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
1690
+ [title]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
1691
+ [(ngModel)]="contacts.phone"
1692
+ />
1693
+ @if (!editMode) {
1694
+ <ion-button
1695
+ slot="end"
1696
+ fill="clear"
1697
+ color="dark"
1698
+ class="marginTop"
1699
+ [title]="'IDEA_UNCOMMON.CONTACTS.CALL' | translate"
1700
+ (click)="call()"
1701
+ >
1702
+ <ion-icon name="call" slot="icon-only" />
1703
+ </ion-button>
1404
1704
  }
1405
- @if (!hideSeconds) {
1406
- <ion-text>
1407
- {{
1408
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase()
1409
- }}
1410
- </ion-text>
1705
+ </ion-item>
1706
+ <ion-item [lines]="lines" [color]="color">
1707
+ <ion-input
1708
+ autocomplete="new"
1709
+ labelPlacement="stacked"
1710
+ [label]="'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate"
1711
+ [disabled]="!editMode"
1712
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
1713
+ [title]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
1714
+ [(ngModel)]="contacts.email"
1715
+ />
1716
+ @if (!editMode) {
1717
+ <ion-button
1718
+ slot="end"
1719
+ fill="clear"
1720
+ color="dark"
1721
+ class="marginTop"
1722
+ [title]="'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate"
1723
+ (click)="sendEmail()"
1724
+ >
1725
+ <ion-icon name="mail" slot="icon-only" />
1726
+ </ion-button>
1411
1727
  }
1412
- </ion-label>
1413
- </ion-item>
1414
- `, isInline: true, 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: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
1728
+ </ion-item>
1729
+ </div>
1730
+ `, isInline: true, styles: [".marginTop{margin-top:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "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", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
1415
1731
  }
1416
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADurationComponent, decorators: [{
1732
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAContactsComponent, decorators: [{
1417
1733
  type: Component,
1418
- args: [{ selector: 'idea-duration', standalone: true, imports: [IonText, IonLabel, IonIcon, IonItem, CommonModule, FormsModule, IDEATranslatePipe], template: `
1419
- <ion-item class="durationItem" [lines]="lines" [color]="color" [title]="title || label">
1420
- @if (icon) {
1421
- <ion-icon slot="start" [name]="icon" />
1422
- }
1423
- @if (!icon && label) {
1424
- <ion-label position="stacked">
1425
- {{ label }}
1426
- @if (obligatory && !disabled) {
1427
- <ion-text class="obligatoryDot" />
1428
- }
1429
- </ion-label>
1734
+ args: [{ selector: 'idea-contacts', standalone: true, imports: [CommonModule, FormsModule, IDEATranslatePipe, IonInput, IonIcon, IonButton, IonItem], template: `
1735
+ <div class="contacts">
1736
+ @if (showName) {
1737
+ <ion-item [lines]="lines" [color]="color">
1738
+ <ion-input
1739
+ autocomplete="new"
1740
+ labelPlacement="stacked"
1741
+ [label]="'IDEA_UNCOMMON.CONTACTS.NAME' | translate"
1742
+ [disabled]="!editMode"
1743
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
1744
+ [title]="'IDEA_UNCOMMON.CONTACTS.NAME_HINT' | translate"
1745
+ [(ngModel)]="contacts.name"
1746
+ />
1747
+ </ion-item>
1430
1748
  }
1431
- <ion-label class="value">
1749
+ <ion-item [lines]="lines" [color]="color">
1432
1750
  <ion-input
1433
- type="number"
1434
- min="0"
1435
- max="23"
1436
- inputmode="numeric"
1437
- [disabled]="disabled"
1438
- [(ngModel)]="hours"
1439
- (ionChange)="setDuration('hours')"
1751
+ autocomplete="new"
1752
+ labelPlacement="stacked"
1753
+ [label]="'IDEA_UNCOMMON.CONTACTS.PHONE' | translate"
1754
+ [disabled]="!editMode"
1755
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
1756
+ [title]="'IDEA_UNCOMMON.CONTACTS.PHONE_HINT' | translate"
1757
+ [(ngModel)]="contacts.phone"
1440
1758
  />
1441
- <ion-text>{{
1442
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()
1443
- }}</ion-text>
1759
+ @if (!editMode) {
1760
+ <ion-button
1761
+ slot="end"
1762
+ fill="clear"
1763
+ color="dark"
1764
+ class="marginTop"
1765
+ [title]="'IDEA_UNCOMMON.CONTACTS.CALL' | translate"
1766
+ (click)="call()"
1767
+ >
1768
+ <ion-icon name="call" slot="icon-only" />
1769
+ </ion-button>
1770
+ }
1771
+ </ion-item>
1772
+ <ion-item [lines]="lines" [color]="color">
1444
1773
  <ion-input
1445
- type="number"
1446
- min="0"
1447
- max="59"
1448
- inputmode="numeric"
1449
- [disabled]="disabled"
1450
- [(ngModel)]="minutes"
1451
- (ionChange)="setDuration('minutes')"
1774
+ autocomplete="new"
1775
+ labelPlacement="stacked"
1776
+ [label]="'IDEA_UNCOMMON.CONTACTS.EMAIL' | translate"
1777
+ [disabled]="!editMode"
1778
+ [placeholder]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
1779
+ [title]="'IDEA_UNCOMMON.CONTACTS.EMAIL_HINT' | translate"
1780
+ [(ngModel)]="contacts.email"
1452
1781
  />
1453
- <ion-text>{{
1454
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()
1455
- }}</ion-text>
1456
- @if (!hideSeconds) {
1457
- <ion-input
1458
- type="number"
1459
- min="0"
1460
- max="59"
1461
- inputmode="numeric"
1462
- [disabled]="disabled"
1463
- [(ngModel)]="seconds"
1464
- (ionChange)="setDuration('seconds')"
1465
- />
1466
- }
1467
- @if (!hideSeconds) {
1468
- <ion-text>
1469
- {{
1470
- ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase()
1471
- }}
1472
- </ion-text>
1782
+ @if (!editMode) {
1783
+ <ion-button
1784
+ slot="end"
1785
+ fill="clear"
1786
+ color="dark"
1787
+ class="marginTop"
1788
+ [title]="'IDEA_UNCOMMON.CONTACTS.SEND_EMAIL' | translate"
1789
+ (click)="sendEmail()"
1790
+ >
1791
+ <ion-icon name="mail" slot="icon-only" />
1792
+ </ion-button>
1473
1793
  }
1474
- </ion-label>
1475
- </ion-item>
1476
- `, 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"] }]
1477
- }], ctorParameters: () => [], propDecorators: { default: [{
1478
- type: Input
1479
- }], label: [{
1480
- type: Input
1481
- }], icon: [{
1482
- type: Input
1483
- }], title: [{
1484
- type: Input
1485
- }], disabled: [{
1486
- type: Input
1487
- }], obligatory: [{
1488
- type: Input
1489
- }], lines: [{
1490
- type: Input
1491
- }], color: [{
1492
- type: Input
1493
- }], hideSeconds: [{
1794
+ </ion-item>
1795
+ </div>
1796
+ `, styles: [".marginTop{margin-top:14px}\n"] }]
1797
+ }], propDecorators: { contacts: [{
1494
1798
  type: Input
1495
- }], shortLabels: [{
1799
+ }], showName: [{
1496
1800
  type: Input
1497
- }], set: [{
1498
- type: Output
1499
- }] } });
1500
-
1501
- class IDEAErrorReportingService {
1502
- constructor() {
1503
- this._env = inject(IDEAEnvironment);
1504
- this._http = inject(HttpClient);
1505
- this._platform = inject(Platform);
1506
- this.apiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
1507
- this.apiUrl = 'https://'.concat([this._env.idea.ideaApi?.url, this.apiStage].filter(x => x).join('/'));
1508
- }
1509
- /**
1510
- * Send the error report to the back-end.
1511
- */
1512
- sendReport(error, forceSend) {
1513
- return new Promise(resolve => {
1514
- // skip in case we don't want to send the report or the report is useless (blank error)
1515
- if ((!this.shouldSend() && !forceSend) || !error?.name)
1516
- return resolve();
1517
- // prepare and send the report
1518
- const report = new ErrorReport({
1519
- version: this._env.idea.app.version,
1520
- stage: this.apiStage,
1521
- client: this.getClientInfo(),
1522
- type: error.name,
1523
- error: error.message,
1524
- stack: error.stack
1525
- });
1526
- this._http
1527
- .post(this.apiUrl.concat(`/projects/${this._env.idea.project}/errorReporting`), report)
1528
- .toPromise()
1529
- .catch(() => { }) // note: never throw an error when reporting an error
1530
- .finally(() => resolve());
1531
- });
1532
- }
1533
- /**
1534
- * Whether we should send the reporting or we are in a scenario in which we should skip it.
1535
- */
1536
- shouldSend() {
1537
- return this.apiStage === 'prod';
1538
- }
1539
- /**
1540
- * Acquire the client's info.
1541
- */
1542
- getClientInfo() {
1543
- return new ClientInfo({
1544
- timestamp: new Date().toISOString(),
1545
- platform: this._platform.platforms().join(' '),
1546
- screenWidth: this._platform.width(),
1547
- screenHeight: this._platform.height(),
1548
- isLandScape: this._platform.isLandscape(),
1549
- url: this._platform.url(),
1550
- referrer: document.referrer,
1551
- isOnline: navigator.onLine,
1552
- language: navigator.language,
1553
- userAgent: navigator.userAgent
1554
- });
1555
- }
1556
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1557
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, providedIn: 'root' }); }
1558
- }
1559
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAErrorReportingService, decorators: [{
1560
- type: Injectable,
1561
- args: [{ providedIn: 'root' }]
1562
- }], ctorParameters: () => [] });
1801
+ }], editMode: [{
1802
+ type: Input
1803
+ }], lines: [{
1804
+ type: Input
1805
+ }], color: [{
1806
+ type: Input
1807
+ }] } });
1563
1808
 
1564
1809
  /**
1565
- * Utility to monitor the network connection.
1566
- * Note: don't use it directly, if you're also using IDEAOfflineDataService.
1810
+ * A random sunday, used as reference to calculate the week days.
1567
1811
  */
1568
- class IDEAOfflineService {
1812
+ const SUNDAY = 259200000;
1813
+ class IDEACalendarPickerComponent {
1569
1814
  constructor() {
1570
- // get an instant status
1571
- this._isOffline = !navigator.onLine;
1572
- if (Network) {
1573
- // set the basic listener for changes
1574
- this.listenerHandler = Network.addListener('networkStatusChange', status => (this._isOffline = !status.connected));
1575
- // create the observable to subscribe to the network changes; note: it won't run until subscribed
1576
- this.observable = new Observable(observer => {
1577
- // remove the basic listener add add one that supports a subscription
1578
- Network.removeAllListeners();
1579
- this.listenerHandler = Network.addListener('networkStatusChange', status => {
1580
- this._isOffline = !status.connected;
1581
- if (observer)
1582
- observer.next(status.connected);
1583
- });
1584
- });
1815
+ this._modal = inject(ModalController);
1816
+ this._alert = inject(AlertController);
1817
+ this._translate = inject(IDEATranslationsService);
1818
+ this.timePicker = false;
1819
+ this.manualTimePicker = false;
1820
+ this.hideClearButton = false;
1821
+ }
1822
+ ngOnInit() {
1823
+ this.today = new Date();
1824
+ this.refDate = this.inputDate ? new Date(this.inputDate) : new Date(this.today);
1825
+ this.selectedDate = new Date(this.refDate);
1826
+ this.hour = 12; // to endure timezones
1827
+ this.minute = 0;
1828
+ if (this.timePicker || this.manualTimePicker) {
1829
+ this.hour = this.selectedDate.getHours();
1830
+ if (this.manualTimePicker)
1831
+ this.minute = this.selectedDate.getMinutes();
1832
+ else {
1833
+ // round the minutes a multiple of 5
1834
+ this.minute = Math.ceil(this.selectedDate.getMinutes() / 5) * 5;
1835
+ this.selectedDate.setMinutes(this.minute);
1836
+ }
1837
+ }
1838
+ this.buildCalendarGrid(this.refDate);
1839
+ this.hours = Array.from(Array(24).keys()).map(i => '0'.concat(i.toString()).slice(-2));
1840
+ this.minutes = Array.from(Array(12).keys()).map(i => '0'.concat((i * 5).toString()).slice(-2));
1841
+ // build the weekdays based on the current locale
1842
+ this.weekDays = new Array();
1843
+ const refDateForWeekDays = new Date(SUNDAY);
1844
+ refDateForWeekDays.setDate(refDateForWeekDays.getDate() - this.getLocalisedDay(refDateForWeekDays));
1845
+ for (let i = 0; i < 7; i++) {
1846
+ this.weekDays.push(refDateForWeekDays.toLocaleDateString(this._translate.getCurrentLang(), { weekday: 'short' }));
1847
+ refDateForWeekDays.setDate(refDateForWeekDays.getDate() + 1);
1585
1848
  }
1849
+ if (this.min)
1850
+ this.dateMin = new Date(this.min);
1851
+ if (this.max)
1852
+ this.dateMax = new Date(this.max);
1586
1853
  }
1587
- /**
1588
- * Subscribe to be notified when the connection status changes.
1589
- */
1590
- subscribe(callback) {
1591
- return this.observable.subscribe(callback);
1854
+ getLocalisedDay(date) {
1855
+ let n = date.getDay();
1856
+ switch (this._translate.getCurrentLang()) {
1857
+ case 'it':
1858
+ n = --n < 0 ? 6 : n;
1859
+ break;
1860
+ }
1861
+ return n;
1592
1862
  }
1593
- /**
1594
- * Quickly check the connection status.
1595
- */
1596
- isOnline() {
1597
- return !this._isOffline;
1863
+ buildCalendarGrid(refDate) {
1864
+ // find the first day in the month: the important data here is the day of the week
1865
+ const firstDateOfMonth = new Date(refDate.getFullYear(), refDate.getMonth(), 1);
1866
+ // the following flag is used to divide the logic so I can fill the calendar
1867
+ // also with the dates from the previous month, until there's space in the grid
1868
+ let haventFoundFirstDay = true;
1869
+ // index used to build the dates of the month, starting from the first one
1870
+ let index = 1;
1871
+ this.calendarGrid = new Array();
1872
+ for (let i = 0; i < 6; i++) {
1873
+ this.calendarGrid[i] = new Array();
1874
+ for (let j = 0; j < 7; j++) {
1875
+ if (haventFoundFirstDay) {
1876
+ if (this.getLocalisedDay(firstDateOfMonth) === j) {
1877
+ // note: considers Sunday
1878
+ haventFoundFirstDay = false;
1879
+ this.calendarGrid[i][j] = new Date(firstDateOfMonth);
1880
+ // now the I've found the first date of the month I can fill the calendar
1881
+ // the dates fromt the previous month, until there's space in the grid
1882
+ for (let y = this.getLocalisedDay(firstDateOfMonth); y >= 0; y--) {
1883
+ const d = new Date(firstDateOfMonth);
1884
+ d.setDate(firstDateOfMonth.getDate() - y);
1885
+ this.calendarGrid[i][this.getLocalisedDay(firstDateOfMonth) - y] = d;
1886
+ }
1887
+ }
1888
+ }
1889
+ else {
1890
+ // fill the following dates until there's space in the grid
1891
+ const d = new Date(firstDateOfMonth);
1892
+ d.setDate(firstDateOfMonth.getDate() + index++);
1893
+ this.calendarGrid[i][j] = d;
1894
+ }
1895
+ }
1896
+ }
1598
1897
  }
1599
- /**
1600
- * Quickly check the connection status.
1601
- */
1602
- isOffline() {
1603
- return this._isOffline;
1898
+ addYears(offset) {
1899
+ this.refDate.setFullYear(this.refDate.getFullYear() + offset);
1900
+ this.buildCalendarGrid(this.refDate);
1604
1901
  }
1605
- /**
1606
- * Quickly check for online connection.
1607
- */
1608
- async check() {
1609
- if (!Network)
1610
- return false;
1611
- const status = await Network.getStatus();
1612
- this._isOffline = !status.connected;
1613
- return status.connected;
1902
+ setYear(year) {
1903
+ if (!year)
1904
+ return;
1905
+ this.refDate.setFullYear(Number(year));
1906
+ this.buildCalendarGrid(this.refDate);
1614
1907
  }
1615
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1616
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, providedIn: 'root' }); }
1617
- }
1618
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAOfflineService, decorators: [{
1619
- type: Injectable,
1620
- args: [{ providedIn: 'root' }]
1621
- }], ctorParameters: () => [] });
1622
-
1623
- /**
1624
- * To communicate with an AWS's API Gateway istance.
1625
- *
1626
- * Includes a rundimental cache on GET requests, until Android doesn't support SWs on local files.
1627
- *
1628
- * Note: requires an `AWSAPIAuthToken` variable to be set by IDEATinCanService service.
1629
- */
1630
- class IDEAAWSAPIService {
1631
- constructor() {
1632
- this._env = inject(IDEAEnvironment);
1633
- this._http = inject(HttpClient);
1634
- this._platform = inject(Platform);
1635
- this._tc = inject(IDEATinCanService);
1636
- this._storage = inject(IDEAStorageService);
1637
- this._errorReporting = inject(IDEAErrorReportingService);
1638
- this._offline = inject(IDEAOfflineService);
1639
- this.apiStage = this._env.idea.api?.stage || this._env.idea.api?.version;
1640
- this.apiUrlProject = 'https://'.concat([this._env.idea.api?.url, this.apiStage].filter(x => x).join('/'));
1641
- this.ideaApiStage = this._env.idea.ideaApi?.stage || this._env.idea.ideaApi?.version;
1642
- this.apiUrlIDEA = 'https://'.concat([this._env.idea.ideaApi?.url, this.ideaApiStage].filter(x => x).join('/'));
1908
+ addMonths(offset) {
1909
+ this.refDate.setMonth(this.refDate.getMonth() + offset);
1910
+ this.buildCalendarGrid(this.refDate);
1643
1911
  }
1644
- /**
1645
- * Execute an online API request.
1646
- * @param resource resource name (e.g. `users`)
1647
- * @param method enum HTTP methods
1648
- * @param options the request options
1649
- */
1650
- request(resource, method, options) {
1651
- return new Promise((resolve, reject) => {
1652
- const opt = options || {};
1653
- // prepare the url
1654
- const url = this.prepareURL(resource, opt);
1655
- // prepare the headers and set the Authorization token
1656
- const headers = this.prepareHeaders(opt.headers);
1657
- // prepare the query params and add some info about the client
1658
- const params = this.prepareQueryParams(opt.params, true);
1659
- // execute the request
1660
- let req;
1661
- switch (method) {
1662
- case 'HEAD':
1663
- req = this._http.head(url, { headers, params });
1664
- break;
1665
- case 'POST':
1666
- req = this._http.post(url, opt.body, { headers, params });
1667
- break;
1668
- case 'PUT':
1669
- req = this._http.put(url, opt.body, { headers, params });
1670
- break;
1671
- case 'PATCH':
1672
- req = this._http.patch(url, opt.body, { headers, params });
1673
- break;
1674
- case 'DELETE':
1675
- req = this._http.delete(url, { headers, params });
1676
- break;
1677
- case 'GET':
1678
- default:
1679
- req = this._http.get(url, { headers, params });
1680
- }
1681
- // handle the request response
1682
- req.subscribe((res) => resolve(res), async (err) => {
1683
- // check if the request failed for network reasons (to trigger offline mode if needed)
1684
- await this._offline.check();
1685
- // (async) send a report, if wanted
1686
- if (opt.reportError)
1687
- this._errorReporting.sendReport(err);
1688
- // fix and return the error
1689
- this.fixErrMessageBeforeReject(err, reject);
1912
+ async showMonths() {
1913
+ const buttons = [];
1914
+ const inputs = [];
1915
+ const month = new Date(0);
1916
+ for (let i = 1; i <= 12; i++) {
1917
+ inputs.push({
1918
+ type: 'radio',
1919
+ label: month.toLocaleDateString(this._translate.getCurrentLang(), { month: 'long' }),
1920
+ value: i.toString(),
1921
+ checked: i === this.refDate.getMonth() + 1
1690
1922
  });
1923
+ month.setMonth(month.getMonth() + 1);
1924
+ }
1925
+ buttons.push({ text: this._translate._('COMMON.CANCEL'), role: 'cancel' });
1926
+ buttons.push({
1927
+ text: this._translate._('COMMON.SELECT'),
1928
+ handler: (m) => {
1929
+ this.refDate.setMonth(parseInt(m, 10) - 1);
1930
+ this.buildCalendarGrid(this.refDate);
1931
+ }
1932
+ });
1933
+ const alert = await this._alert.create({
1934
+ header: this._translate._('IDEA_UNCOMMON.CALENDAR.MONTH'),
1935
+ buttons,
1936
+ inputs
1691
1937
  });
1938
+ alert.present();
1939
+ }
1940
+ isDateSelectable(date) {
1941
+ if (this.dateMin && date < this.dateMin)
1942
+ return false;
1943
+ if (this.dateMax && date > this.dateMax)
1944
+ return false;
1945
+ return true;
1946
+ }
1947
+ selectDate(date) {
1948
+ this.selectedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), this.hour, this.minute);
1949
+ }
1950
+ selectHour(hour) {
1951
+ this.hour = Number(hour);
1952
+ this.selectedDate.setHours(this.hour);
1953
+ }
1954
+ selectMinute(minute) {
1955
+ this.minute = Number(minute);
1956
+ this.selectedDate.setMinutes(this.minute);
1692
1957
  }
1693
- /**
1694
- * Prepare the URL to which execute the request.
1695
- */
1696
- prepareURL(resource, opt) {
1697
- // decide whether to use IDEA's API or the project's API (or an alternative API)
1698
- let url = opt.idea ? this.apiUrlIDEA : opt.alternativeAPI || this.apiUrlProject;
1699
- // prepare a single-resource request (by id) or a normal one
1700
- url = `${url}/${resource}`;
1701
- if (opt.resourceId)
1702
- url = `${url}/${encodeURIComponent(opt.resourceId)}`;
1703
- return url;
1958
+ isSameDay(a, b) {
1959
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1704
1960
  }
1705
- /**
1706
- * Prepare the headers object to send with a request.
1707
- * Note: it always adds the Authorization token (`AWSAPIAuthToken`) and the API key (`AWSAPIKey`), if set.
1708
- */
1709
- prepareHeaders(h) {
1710
- // preare the headers; note: HttpHeaders is immutable!
1711
- let headers = new HttpHeaders(h || null);
1712
- // set the Authorization token
1713
- if (!headers.get('Authorization') && this._tc.get('AWSAPIAuthToken'))
1714
- headers = headers.set('Authorization', this._tc.get('AWSAPIAuthToken'));
1715
- // set the API key
1716
- if (!headers.get('X-API-Key') && this._tc.get('AWSAPIKey'))
1717
- headers = headers.set('X-API-Key', this._tc.get('AWSAPIKey'));
1718
- return headers;
1961
+ isSameMonth(a, b) {
1962
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
1719
1963
  }
1720
- /**
1721
- * Prepare the queryParams object to send with a request; if requested, add some info about the client.
1722
- */
1723
- prepareQueryParams(qp, addClientInfo) {
1724
- // prepare the query params; note: HttpParams is immutable!
1725
- let searchParams = new HttpParams();
1726
- if (qp && qp instanceof HttpParams)
1727
- searchParams = qp;
1728
- else if (qp)
1729
- for (const prop in qp)
1730
- if (qp[prop])
1731
- searchParams = searchParams.set(prop, qp[prop]);
1732
- // if requested, add app version and client platform to the info we send to the back-end
1733
- if (addClientInfo) {
1734
- searchParams = searchParams.set('_v', this._env.idea.app.version);
1735
- searchParams = searchParams.set('_p', this._platform.platforms().join(' '));
1736
- }
1737
- return searchParams;
1964
+ isSameHour(hour) {
1965
+ return this.selectedDate.getHours() === Number(hour);
1738
1966
  }
1739
- /**
1740
- * Converts the error message to be readable.
1741
- */
1742
- fixErrMessageBeforeReject(err, reject) {
1743
- let e;
1744
- try {
1745
- if (!err || !err.error)
1746
- e = {};
1747
- else if (typeof err.error === 'string')
1748
- e = JSON.parse(err.error);
1749
- else
1750
- e = err.error;
1751
- }
1752
- catch (_) {
1753
- e = {};
1754
- }
1755
- reject(new Error(e.message || 'Unknown error!'));
1967
+ isSameMinute(minute) {
1968
+ return this.selectedDate.getMinutes() === Number(minute);
1756
1969
  }
1757
- /**
1758
- * Use the HttpClient to execute a raw request.
1759
- */
1760
- rawRequest() {
1761
- return this._http;
1970
+ save(reset) {
1971
+ this._modal.dismiss(reset ? '' : this.selectedDate);
1762
1972
  }
1763
- /**
1764
- * HEAD request.
1765
- * @param resource resource name (e.g. `users`)
1766
- * @param options the request options
1767
- */
1768
- headResource(resource, options) {
1769
- return this.request(resource, 'HEAD', options);
1973
+ close() {
1974
+ this._modal.dismiss();
1770
1975
  }
1771
- /**
1772
- * GET request. Include options to cache the requests (`options.useCache`), to work also offline.
1773
- * @param resource resource name (e.g. `users`)
1774
- * @param options the request options
1775
- */
1776
- getResource(resource, options) {
1777
- return new Promise((resolve, reject) => {
1778
- const opt = options || {};
1779
- // if offline and with a cache mode set, force the request to the cache
1780
- if (!navigator.onLine && opt.useCache)
1781
- opt.useCache = CacheModes.CACHE_ONLY;
1782
- // execute the GET request online or through the cache, depending on the chosen mode
1783
- switch (opt.useCache) {
1784
- case CacheModes.CACHE_ONLY:
1785
- // return the result from the cache, without retrying online if a result wasn't found
1786
- this.getFromCache(resource, opt).then((res) => {
1787
- if (res)
1788
- resolve(res);
1789
- else
1790
- reject();
1791
- });
1792
- break;
1793
- case CacheModes.CACHE_FIRST:
1794
- // return the result from the cache
1795
- this.getFromCache(resource, opt).then((localRes) => {
1796
- // check whether the local resource is present (element or list)
1797
- if (localRes && (opt.resourceId || localRes.length)) {
1798
- resolve(localRes);
1799
- // asynchrounously execute the request online, to update the cache with latest data
1800
- this.request(resource, 'GET', opt)
1801
- .then((cloudRes) => {
1802
- // update the cache (if it fails, it's ok)
1803
- this.putInCache(resource, cloudRes, opt).catch(() => { });
1804
- })
1805
- .catch(() => { }); // we already returned the result, an error is acceptable
1806
- }
1807
- else {
1808
- // if the result isn't found in the cache, return the result of the online request
1809
- this.request(resource, 'GET', opt)
1810
- .then((res) => {
1811
- resolve(res);
1812
- // update the cache (if it fails, it's ok)
1813
- this.putInCache(resource, res, opt).catch(() => { });
1814
- })
1815
- .catch((err) => reject(err)); // element not found
1816
- }
1817
- });
1818
- break;
1819
- case CacheModes.NETWORK_FIRST:
1820
- // return the result from an online request
1821
- this.request(resource, 'GET', opt)
1822
- .then((cloudRes) => {
1823
- resolve(cloudRes);
1824
- // asynchrounously get the same element from cache and decide whether to update or not
1825
- this.getFromCache(resource, opt).then(() => {
1826
- // update the cache (if it fails, it's ok)
1827
- this.putInCache(resource, cloudRes, opt).catch(() => { });
1828
- });
1829
- })
1830
- .catch(() => {
1831
- // if a request to the network fails, check in cache
1832
- this.getFromCache(resource, opt).then((res) => {
1833
- if (res)
1834
- resolve(res);
1835
- else
1836
- reject();
1837
- });
1838
- });
1839
- break;
1840
- default:
1841
- /* CacheModes.NO_CACHE */
1842
- // return the result from an online request, with no cache involved
1843
- this.request(resource, 'GET', opt)
1844
- .then((res) => resolve(res))
1845
- .catch((err) => reject(err));
1976
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEACalendarPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1977
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEACalendarPickerComponent, isStandalone: true, 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: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "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", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: 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: IonRow, selector: "ion-row" }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }, { kind: "pipe", type: IDEALocalizedDatePipe, name: "dateLocale" }] }); }
1978
+ }
1979
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEACalendarPickerComponent, decorators: [{
1980
+ type: Component,
1981
+ args: [{ selector: 'idea-calendar-picker', standalone: true, imports: [
1982
+ CommonModule,
1983
+ FormsModule,
1984
+ IDEATranslatePipe,
1985
+ IDEALocalizedDatePipe,
1986
+ IonFooter,
1987
+ IonInput,
1988
+ IonCol,
1989
+ IonRow,
1990
+ IonContent,
1991
+ IonGrid,
1992
+ IonIcon,
1993
+ IonButton,
1994
+ IonButtons,
1995
+ IonTitle,
1996
+ IonToolbar,
1997
+ IonHeader
1998
+ ], 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"] }]
1999
+ }], propDecorators: { inputDate: [{
2000
+ type: Input
2001
+ }], timePicker: [{
2002
+ type: Input
2003
+ }], manualTimePicker: [{
2004
+ type: Input
2005
+ }], title: [{
2006
+ type: Input
2007
+ }], hideClearButton: [{
2008
+ type: Input
2009
+ }], min: [{
2010
+ type: Input
2011
+ }], max: [{
2012
+ type: Input
2013
+ }] } });
2014
+
2015
+ class IDEADateTimeComponent {
2016
+ constructor() {
2017
+ this._modal = inject(ModalController);
2018
+ this._translate = inject(IDEATranslationsService);
2019
+ /**
2020
+ * Whether to show the time picker (datetime) or not (date).
2021
+ */
2022
+ this.timePicker = false;
2023
+ /**
2024
+ * Whether to show the MANUAL time picker (datetime) or not (date).
2025
+ */
2026
+ this.manualTimePicker = false;
2027
+ /**
2028
+ * Whether to use the `epochISOString` format instead of `epochDateTime`.
2029
+ */
2030
+ this.useISOFormat = false;
2031
+ /**
2032
+ * If true, the component is disabled.
2033
+ */
2034
+ this.disabled = false;
2035
+ /**
2036
+ * If true, the obligatory dot is shown.
2037
+ */
2038
+ this.obligatory = false;
2039
+ /**
2040
+ * If true, hidew the clear button in the header.
2041
+ */
2042
+ this.hideClearButton = false;
2043
+ this.dateChange = new EventEmitter();
2044
+ this.iconSelect = new EventEmitter();
2045
+ this.isOpening = false;
2046
+ }
2047
+ ngOnInit() {
2048
+ this.langChangeSubscription = this._translate.onLangChange.subscribe(() => {
2049
+ this.valueToDisplay = this.getValueToDisplay(this.date);
2050
+ });
2051
+ }
2052
+ ngOnDestroy() {
2053
+ if (this.langChangeSubscription)
2054
+ this.langChangeSubscription.unsubscribe();
2055
+ }
2056
+ ngOnChanges(changes) {
2057
+ if (changes.date || changes.timePicker)
2058
+ this.valueToDisplay = this.getValueToDisplay(this.date);
2059
+ }
2060
+ async openCalendarPicker() {
2061
+ if (this.disabled || this.isOpening)
2062
+ return;
2063
+ this.isOpening = true;
2064
+ const modal = await this._modal.create({
2065
+ component: IDEACalendarPickerComponent,
2066
+ componentProps: {
2067
+ inputDate: this.date,
2068
+ title: this.label,
2069
+ timePicker: this.timePicker,
2070
+ manualTimePicker: this.manualTimePicker,
2071
+ hideClearButton: this.hideClearButton,
2072
+ min: this.min,
2073
+ max: this.max
1846
2074
  }
1847
2075
  });
1848
- }
1849
- /**
1850
- * Observe a GET request. Include options to cache the requests (`options.useCache`), to work also offline.
1851
- * @param resource resource name (e.g. `users`)
1852
- * @param options the request options
1853
- */
1854
- getResourceObserver(resource, options) {
1855
- return new Observable(observer => {
1856
- const opt = options || {};
1857
- // if offline and with a cache mode set, force the request to the cache
1858
- if (!navigator.onLine && opt.useCache)
1859
- opt.useCache = CacheModes.CACHE_ONLY;
1860
- // execute the GET request online or through the cache, depending on the chosen mode
1861
- switch (opt.useCache) {
1862
- case CacheModes.CACHE_ONLY:
1863
- // return the result from the cache, without retrying online if a result wasn't found
1864
- this.getFromCache(resource, opt).then((res) => {
1865
- if (res)
1866
- observer.next(res);
1867
- else
1868
- observer.error();
1869
- });
1870
- break;
1871
- case CacheModes.CACHE_FIRST:
1872
- // return the result from the cache
1873
- this.getFromCache(resource, opt).then((localRes) => {
1874
- if (localRes) {
1875
- observer.next(localRes);
1876
- // asynchrounously execute the request online, to update the cache with latest data
1877
- this.request(resource, 'GET', opt)
1878
- .then((cloudRes) => {
1879
- observer.next(cloudRes);
1880
- // update the cache (if it fails, it's ok)
1881
- this.putInCache(resource, cloudRes, opt).catch(() => { });
1882
- })
1883
- .catch(() => { }); // we already returned the result, an error is acceptable
1884
- }
1885
- else {
1886
- // if the result isn't found in the cache, return the result of the online request
1887
- this.request(resource, 'GET', opt)
1888
- .then((res) => {
1889
- observer.next(res);
1890
- // update the cache (if it fails, it's ok)
1891
- this.putInCache(resource, res, opt).catch(() => { });
1892
- })
1893
- .catch((err) => observer.error(err)); // element not found
1894
- }
1895
- });
1896
- break;
1897
- case CacheModes.NETWORK_FIRST:
1898
- // return the result from an online request
1899
- this.request(resource, 'GET', opt)
1900
- .then((cloudRes) => {
1901
- observer.next(cloudRes);
1902
- // update the cache (if it fails, it's ok)
1903
- this.putInCache(resource, cloudRes, opt).catch(() => { });
1904
- })
1905
- .catch(() => {
1906
- // if a request to the network fails, check in cache
1907
- this.getFromCache(resource, opt).then((res) => {
1908
- if (res)
1909
- observer.next(res);
1910
- else
1911
- observer.error();
1912
- });
1913
- });
1914
- break;
1915
- default:
1916
- /* CacheModes.NO_CACHE */
1917
- // return the result from an online request, with no cache involved
1918
- this.request(resource, 'GET', opt)
1919
- .then((res) => observer.next(res))
1920
- .catch((err) => observer.error(err));
2076
+ modal.onDidDismiss().then(({ data }) => {
2077
+ if (data !== undefined && data !== null) {
2078
+ if (data === '')
2079
+ this.doSelect(null);
2080
+ else
2081
+ this.doSelect(data);
1921
2082
  }
1922
2083
  });
2084
+ modal.present();
2085
+ this.isOpening = false;
1923
2086
  }
1924
- /**
1925
- * Execute a GET request from the local storage.
1926
- * @param resource resource name (e.g. `users`)
1927
- * @param options the request options
1928
- */
1929
- getFromCache(resource, options) {
1930
- return new Promise(resolve => {
1931
- const opt = options || {};
1932
- // prepare the url and the query params
1933
- const url = this.prepareURL(resource, opt);
1934
- const queryParams = this.prepareQueryParams(opt.params);
1935
- // get from storage, by the complete url
1936
- this._storage
1937
- .get(url.concat(queryParams.toString()))
1938
- .then((res) => (res ? resolve(res) : opt.resourceId ? resolve(null) : resolve([])));
1939
- });
1940
- }
1941
- /**
1942
- * Execute a PUT request in the local storage.
1943
- * @param resource resource name (e.g. `users`)
1944
- * @param data resource data
1945
- * @param options the request options
1946
- */
1947
- putInCache(resource, data, options) {
1948
- return new Promise((resolve, reject) => {
1949
- const opt = options || {};
1950
- // prepare the url and the query params
1951
- const url = this.prepareURL(resource, opt);
1952
- const queryParams = this.prepareQueryParams(opt.params);
1953
- // put in the storage, by the complete url
1954
- this._storage
1955
- .set(url.concat(queryParams.toString()), data)
1956
- .then(() => resolve())
1957
- .catch((err) => reject(err));
1958
- });
2087
+ getValueToDisplay(date) {
2088
+ return !date
2089
+ ? ''
2090
+ : this._translate.formatDate(date, this.timePicker || this.manualTimePicker ? 'd MMM yyyy, HH:mm' : 'mediumDate');
1959
2091
  }
1960
- /**
1961
- * Execute a DELETE request in the local storage.
1962
- * @param resource resource name (e.g. `users`)
1963
- * @param options the request options
1964
- */
1965
- deleteFromCache(resource, options) {
1966
- return new Promise((resolve, reject) => {
1967
- const opt = options || {};
1968
- // prepare the url and the query params
1969
- const url = this.prepareURL(resource, opt);
1970
- const queryParams = this.prepareQueryParams(opt.params);
1971
- // delete from storage, by the complete url
1972
- this._storage
1973
- .remove(url.concat(queryParams.toString()))
1974
- .then(() => resolve())
1975
- .catch((err) => reject(err));
1976
- });
2092
+ doSelect(date) {
2093
+ this.dateChange.emit(date ? (this.useISOFormat ? date.toISOString() : date.valueOf()) : null);
1977
2094
  }
1978
- /**
1979
- * POST request.
1980
- * @param resource resource name (e.g. `users`)
1981
- * @param options the request options
1982
- */
1983
- postResource(resource, options) {
1984
- return this.request(resource, 'POST', options);
2095
+ doIconSelect(event) {
2096
+ if (event)
2097
+ event.stopPropagation();
2098
+ this.iconSelect.emit();
1985
2099
  }
1986
- /**
1987
- * PUT request.
1988
- * @param resource resource name (e.g. `users`)
1989
- * @param options the request options
1990
- */
1991
- putResource(resource, options) {
1992
- return this.request(resource, 'PUT', options);
2100
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADateTimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2101
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEADateTimeComponent, isStandalone: true, 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: `
2102
+ <ion-item
2103
+ class="dateTimeItem"
2104
+ [color]="color"
2105
+ [lines]="lines"
2106
+ [title]="placeholder || label || ''"
2107
+ [button]="!disabled"
2108
+ [disabled]="isOpening"
2109
+ [class.withLabel]="label"
2110
+ (click)="openCalendarPicker()"
2111
+ >
2112
+ @if (icon) {
2113
+ <ion-button
2114
+ fill="clear"
2115
+ slot="start"
2116
+ [color]="iconColor"
2117
+ [class.marginTop]="label"
2118
+ (click)="doIconSelect($event)"
2119
+ >
2120
+ <ion-icon [icon]="icon" slot="icon-only" />
2121
+ </ion-button>
2122
+ }
2123
+ @if (label) {
2124
+ <ion-label position="stacked" [class.selectable]="!disabled">
2125
+ {{ label }}
2126
+ @if (obligatory && !disabled) {
2127
+ <ion-text class="obligatoryDot" />
2128
+ }
2129
+ </ion-label>
2130
+ }
2131
+ <ion-label class="value" [class.selectable]="!disabled">
2132
+ @if (!valueToDisplay && !disabled) {
2133
+ <ion-text class="placeholder" [class.selectable]="!disabled">
2134
+ {{ placeholder }}
2135
+ </ion-text>
2136
+ }
2137
+ {{ valueToDisplay }}
2138
+ </ion-label>
2139
+ @if (!disabled) {
2140
+ <ion-icon slot="end" icon="caret-down" class="selectIcon" [class.selectable]="!disabled" />
2141
+ }
2142
+ </ion-item>
2143
+ `, isInline: true, 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: "ngmodule", type: CommonModule }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }] }); }
2144
+ }
2145
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADateTimeComponent, decorators: [{
2146
+ type: Component,
2147
+ args: [{ selector: 'idea-date-time', standalone: true, imports: [CommonModule, IonText, IonLabel, IonIcon, IonButton, IonItem], template: `
2148
+ <ion-item
2149
+ class="dateTimeItem"
2150
+ [color]="color"
2151
+ [lines]="lines"
2152
+ [title]="placeholder || label || ''"
2153
+ [button]="!disabled"
2154
+ [disabled]="isOpening"
2155
+ [class.withLabel]="label"
2156
+ (click)="openCalendarPicker()"
2157
+ >
2158
+ @if (icon) {
2159
+ <ion-button
2160
+ fill="clear"
2161
+ slot="start"
2162
+ [color]="iconColor"
2163
+ [class.marginTop]="label"
2164
+ (click)="doIconSelect($event)"
2165
+ >
2166
+ <ion-icon [icon]="icon" slot="icon-only" />
2167
+ </ion-button>
2168
+ }
2169
+ @if (label) {
2170
+ <ion-label position="stacked" [class.selectable]="!disabled">
2171
+ {{ label }}
2172
+ @if (obligatory && !disabled) {
2173
+ <ion-text class="obligatoryDot" />
2174
+ }
2175
+ </ion-label>
2176
+ }
2177
+ <ion-label class="value" [class.selectable]="!disabled">
2178
+ @if (!valueToDisplay && !disabled) {
2179
+ <ion-text class="placeholder" [class.selectable]="!disabled">
2180
+ {{ placeholder }}
2181
+ </ion-text>
2182
+ }
2183
+ {{ valueToDisplay }}
2184
+ </ion-label>
2185
+ @if (!disabled) {
2186
+ <ion-icon slot="end" icon="caret-down" class="selectIcon" [class.selectable]="!disabled" />
2187
+ }
2188
+ </ion-item>
2189
+ `, 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"] }]
2190
+ }], propDecorators: { date: [{
2191
+ type: Input
2192
+ }], timePicker: [{
2193
+ type: Input
2194
+ }], manualTimePicker: [{
2195
+ type: Input
2196
+ }], useISOFormat: [{
2197
+ type: Input
2198
+ }], label: [{
2199
+ type: Input
2200
+ }], icon: [{
2201
+ type: Input
2202
+ }], iconColor: [{
2203
+ type: Input
2204
+ }], lines: [{
2205
+ type: Input
2206
+ }], color: [{
2207
+ type: Input
2208
+ }], placeholder: [{
2209
+ type: Input
2210
+ }], disabled: [{
2211
+ type: Input
2212
+ }], obligatory: [{
2213
+ type: Input
2214
+ }], hideClearButton: [{
2215
+ type: Input
2216
+ }], min: [{
2217
+ type: Input
2218
+ }], max: [{
2219
+ type: Input
2220
+ }], dateChange: [{
2221
+ type: Output
2222
+ }], iconSelect: [{
2223
+ type: Output
2224
+ }] } });
2225
+
2226
+ class IDEADurationComponent {
2227
+ constructor() {
2228
+ /**
2229
+ * On change event. It emits a number of seconds representing the duration.
2230
+ */
2231
+ this.set = new EventEmitter();
2232
+ this.default = this.hours = this.minutes = this.seconds = 0;
1993
2233
  }
1994
- /**
1995
- * PATCH request.
1996
- * @param resource resource name (e.g. `users`)
1997
- * @param options the request options
1998
- */
1999
- patchResource(resource, options) {
2000
- return this.request(resource, 'PATCH', options);
2234
+ ngOnChanges(changes) {
2235
+ // assign a default value to the duration
2236
+ if (changes['default'] && changes['default'].isFirstChange()) {
2237
+ const refDate = new Date(0);
2238
+ refDate.setHours(0, 0, 0);
2239
+ refDate.setSeconds(refDate.getSeconds() + this.default);
2240
+ this.hours = refDate.getHours();
2241
+ this.minutes = refDate.getMinutes();
2242
+ this.seconds = this.hideSeconds ? 0 : refDate.getSeconds();
2243
+ }
2001
2244
  }
2002
2245
  /**
2003
- * DELETE request.
2004
- * @param resource resource name (e.g. `users`)
2005
- * @param options the request options
2246
+ * When one of the parts changes, emit the new duration.
2006
2247
  */
2007
- deleteResource(resource, options) {
2008
- return this.request(resource, 'DELETE', options);
2248
+ setDuration(type) {
2249
+ switch (type) {
2250
+ case 'hours':
2251
+ const hh = Number(this.hours) || 0;
2252
+ this.hours = hh > 23 ? 23 : hh < 0 ? 0 : hh;
2253
+ break;
2254
+ case 'minutes':
2255
+ const mm = Number(this.minutes) || 0;
2256
+ this.minutes = mm > 59 ? 59 : mm < 0 ? 0 : mm;
2257
+ break;
2258
+ case 'seconds':
2259
+ const ss = Number(this.seconds) || 0;
2260
+ if (this.hideSeconds)
2261
+ this.seconds = 0;
2262
+ else
2263
+ this.seconds = ss > 59 ? 59 : ss < 0 ? 0 : ss;
2264
+ break;
2265
+ }
2266
+ this.set.emit(this.hours * 3600 + this.minutes * 60 + this.seconds);
2009
2267
  }
2010
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2011
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, providedIn: 'root' }); }
2012
- }
2013
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAWSAPIService, decorators: [{
2014
- type: Injectable,
2015
- args: [{ providedIn: 'root' }]
2016
- }], ctorParameters: () => [] });
2017
- /**
2018
- * The options of an API request.
2019
- */
2020
- class APIRequestOption {
2268
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2269
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEADurationComponent, isStandalone: true, 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: `
2270
+ <ion-item class="durationItem" [lines]="lines" [color]="color" [title]="title || label">
2271
+ @if (icon) {
2272
+ <ion-icon slot="start" [name]="icon" />
2273
+ }
2274
+ @if (!icon && label) {
2275
+ <ion-label position="stacked">
2276
+ {{ label }}
2277
+ @if (obligatory && !disabled) {
2278
+ <ion-text class="obligatoryDot" />
2279
+ }
2280
+ </ion-label>
2281
+ }
2282
+ <ion-label class="value">
2283
+ <ion-input
2284
+ type="number"
2285
+ min="0"
2286
+ max="23"
2287
+ inputmode="numeric"
2288
+ [disabled]="disabled"
2289
+ [(ngModel)]="hours"
2290
+ (ionChange)="setDuration('hours')"
2291
+ />
2292
+ <ion-text>{{
2293
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()
2294
+ }}</ion-text>
2295
+ <ion-input
2296
+ type="number"
2297
+ min="0"
2298
+ max="59"
2299
+ inputmode="numeric"
2300
+ [disabled]="disabled"
2301
+ [(ngModel)]="minutes"
2302
+ (ionChange)="setDuration('minutes')"
2303
+ />
2304
+ <ion-text>{{
2305
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()
2306
+ }}</ion-text>
2307
+ @if (!hideSeconds) {
2308
+ <ion-input
2309
+ type="number"
2310
+ min="0"
2311
+ max="59"
2312
+ inputmode="numeric"
2313
+ [disabled]="disabled"
2314
+ [(ngModel)]="seconds"
2315
+ (ionChange)="setDuration('seconds')"
2316
+ />
2317
+ }
2318
+ @if (!hideSeconds) {
2319
+ <ion-text>
2320
+ {{
2321
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase()
2322
+ }}
2323
+ </ion-text>
2324
+ }
2325
+ </ion-label>
2326
+ </ion-item>
2327
+ `, isInline: true, 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: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
2021
2328
  }
2022
- /**
2023
- * Set of usable cache modes.
2024
- */
2025
- var CacheModes;
2026
- (function (CacheModes) {
2027
- /**
2028
- * Return the result from an online request, with no cache involved.
2029
- */
2030
- CacheModes[CacheModes["NO_CACHE"] = 0] = "NO_CACHE";
2031
- /**
2032
- * Return the result from an online request, storing the result in the cache, to update it with the latest data.
2033
- */
2034
- CacheModes[CacheModes["NETWORK_FIRST"] = 1] = "NETWORK_FIRST";
2035
- /**
2036
- * Return the result from the cache, but also execute the request online, to update the cache with the latest data.
2037
- */
2038
- CacheModes[CacheModes["CACHE_FIRST"] = 2] = "CACHE_FIRST";
2039
- /**
2040
- * Return the result from the cache, without retrying online if a result wasn't found.
2041
- */
2042
- CacheModes[CacheModes["CACHE_ONLY"] = 3] = "CACHE_ONLY";
2043
- })(CacheModes || (CacheModes = {}));
2329
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEADurationComponent, decorators: [{
2330
+ type: Component,
2331
+ args: [{ selector: 'idea-duration', standalone: true, imports: [IonText, IonLabel, IonIcon, IonItem, CommonModule, FormsModule, IDEATranslatePipe], template: `
2332
+ <ion-item class="durationItem" [lines]="lines" [color]="color" [title]="title || label">
2333
+ @if (icon) {
2334
+ <ion-icon slot="start" [name]="icon" />
2335
+ }
2336
+ @if (!icon && label) {
2337
+ <ion-label position="stacked">
2338
+ {{ label }}
2339
+ @if (obligatory && !disabled) {
2340
+ <ion-text class="obligatoryDot" />
2341
+ }
2342
+ </ion-label>
2343
+ }
2344
+ <ion-label class="value">
2345
+ <ion-input
2346
+ type="number"
2347
+ min="0"
2348
+ max="23"
2349
+ inputmode="numeric"
2350
+ [disabled]="disabled"
2351
+ [(ngModel)]="hours"
2352
+ (ionChange)="setDuration('hours')"
2353
+ />
2354
+ <ion-text>{{
2355
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.HH' : 'IDEA_UNCOMMON.DURATION.HOURS') | translate).toLowerCase()
2356
+ }}</ion-text>
2357
+ <ion-input
2358
+ type="number"
2359
+ min="0"
2360
+ max="59"
2361
+ inputmode="numeric"
2362
+ [disabled]="disabled"
2363
+ [(ngModel)]="minutes"
2364
+ (ionChange)="setDuration('minutes')"
2365
+ />
2366
+ <ion-text>{{
2367
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.MM' : 'IDEA_UNCOMMON.DURATION.MINUTES') | translate).toLowerCase()
2368
+ }}</ion-text>
2369
+ @if (!hideSeconds) {
2370
+ <ion-input
2371
+ type="number"
2372
+ min="0"
2373
+ max="59"
2374
+ inputmode="numeric"
2375
+ [disabled]="disabled"
2376
+ [(ngModel)]="seconds"
2377
+ (ionChange)="setDuration('seconds')"
2378
+ />
2379
+ }
2380
+ @if (!hideSeconds) {
2381
+ <ion-text>
2382
+ {{
2383
+ ((shortLabels ? 'IDEA_UNCOMMON.DURATION.SS' : 'IDEA_UNCOMMON.DURATION.SECONDS') | translate).toLowerCase()
2384
+ }}
2385
+ </ion-text>
2386
+ }
2387
+ </ion-label>
2388
+ </ion-item>
2389
+ `, 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"] }]
2390
+ }], ctorParameters: () => [], propDecorators: { default: [{
2391
+ type: Input
2392
+ }], label: [{
2393
+ type: Input
2394
+ }], icon: [{
2395
+ type: Input
2396
+ }], title: [{
2397
+ type: Input
2398
+ }], disabled: [{
2399
+ type: Input
2400
+ }], obligatory: [{
2401
+ type: Input
2402
+ }], lines: [{
2403
+ type: Input
2404
+ }], color: [{
2405
+ type: Input
2406
+ }], hideSeconds: [{
2407
+ type: Input
2408
+ }], shortLabels: [{
2409
+ type: Input
2410
+ }], set: [{
2411
+ type: Output
2412
+ }] } });
2044
2413
 
2045
2414
  class IDEAEchoPage {
2046
2415
  constructor() {
@@ -3901,5 +4270,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImpor
3901
4270
  * Generated bundle index. Do not edit.
3902
4271
  */
3903
4272
 
3904
- export { APIRequestOption, CacheModes, CacheableResource, ColorsPaletteComponent, EchoRequests, IDEAAWSAPIService, IDEAAWSAPISocketService, IDEAAddressComponent, IDEAAnnouncementComponent, IDEACacheableResourceComponent, IDEACachedResourcesService, IDEACalendarPickerComponent, IDEAColorPickerComponent, IDEAContactsComponent, IDEADateTimeComponent, IDEADurationComponent, IDEAEchoPage, IDEAErrorReportingService, IDEAFromTimeToTimeComponent, IDEAMDEComponent, IDEAMDEToolbarComponent, IDEAOfflineDataService, IDEAOfflineIndicatorComponent, IDEAOfflineManagerComponent, IDEAOfflineService, IDEAPushNotificationsService, IDEASentimentComponent, IDEATimeIntervalComponent, IDEATinCanService, LAST_SYNC_KEY, NUM_ELEMENTS_WITHOUT_MANUAL_SYNC, NUM_ELEMENTS_WITH_MANUAL_SYNC, Periods, QUEUE_API_REQUESTS_KEY, SYNC_EXPIRATION_INTERVAL, ideaEchoRoutes };
4273
+ export { APIRequestOption, CacheModes, CacheableResource, ColorsPaletteComponent, EchoRequests, IDEAAWSAPIService, IDEAAWSAPISocketService, IDEAAddressComponent, IDEAAnnouncementComponent, IDEACacheableResourceComponent, IDEACachedResourcesService, IDEACalendarPickerComponent, IDEAColorPickerComponent, IDEAContactsComponent, IDEADateTimeComponent, IDEADurationComponent, IDEAEchoPage, IDEAErrorReportingService, IDEAFromTimeToTimeComponent, IDEAMDEComponent, IDEAMDEToolbarComponent, IDEAOfflineDataService, IDEAOfflineIndicatorComponent, IDEAOfflineManagerComponent, IDEAOfflineService, IDEAOldAttachmentsComponent, IDEAPushNotificationsService, IDEASentimentComponent, IDEATimeIntervalComponent, IDEATinCanService, LAST_SYNC_KEY, NUM_ELEMENTS_WITHOUT_MANUAL_SYNC, NUM_ELEMENTS_WITH_MANUAL_SYNC, Periods, QUEUE_API_REQUESTS_KEY, SYNC_EXPIRATION_INTERVAL, ideaEchoRoutes };
3905
4274
  //# sourceMappingURL=idea-ionic-uncommon.mjs.map