diff --git a/PRODUCTION_DEPENDENCIES.md b/PRODUCTION_DEPENDENCIES.md index 7baedbfc25..fc52769b6d 100644 --- a/PRODUCTION_DEPENDENCIES.md +++ b/PRODUCTION_DEPENDENCIES.md @@ -37,7 +37,7 @@ | [date-fns-tz](https://github.com/marnusw/date-fns-tz) | 2.0.0 | MIT | | [date-fns](https://github.com/date-fns/date-fns) | 2.29.2 | MIT | | [dayjs](https://github.com/iamkun/dayjs) | 1.11.10 | MIT | -| [express](https://github.com/expressjs/express) | 4.19.2 | MIT | +| [express](https://github.com/expressjs/express) | 4.21.0 | MIT | | [graphql-sse](https://github.com/enisdenjo/graphql-sse) | 2.5.3 | MIT | | [graphql](https://github.com/graphql/graphql-js) | 16.8.1 | MIT | | [highlight.js](https://github.com/highlightjs/highlight.js) | 11.10.0 | BSD-3-Clause | diff --git a/apps/dh/api-dh/source/DataHub.WebApi.Tests/Snapshots/SchemaTests.ChangeTest.verified.graphql b/apps/dh/api-dh/source/DataHub.WebApi.Tests/Snapshots/SchemaTests.ChangeTest.verified.graphql index 2846abe763..57f4a26324 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi.Tests/Snapshots/SchemaTests.ChangeTest.verified.graphql +++ b/apps/dh/api-dh/source/DataHub.WebApi.Tests/Snapshots/SchemaTests.ChangeTest.verified.graphql @@ -542,7 +542,7 @@ type SettlementReport { statusType: SettlementReportStatusType! executionTime: DateRange! fromApi: Boolean! - splitReportPerGridArea: Boolean! + combineResultInASingleFile: Boolean! includeMonthlyAmount: Boolean! gridAreas: [String!]! } diff --git a/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/ISettlementReportsClient.cs b/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/ISettlementReportsClient.cs index d2a94c56e4..5d1926709a 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/ISettlementReportsClient.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/ISettlementReportsClient.cs @@ -36,5 +36,5 @@ public interface ISettlementReportsClient /// Downloads the settlement report with the specified id. /// /// The stream to the report. - public Task DownloadAsync(SettlementReportRequestId requestId, CancellationToken cancellationToken); + public Task DownloadAsync(SettlementReportRequestId requestId, bool fromApi, CancellationToken cancellationToken); } diff --git a/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/SettlementReportsClient.cs b/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/SettlementReportsClient.cs index 5e74f2c197..7694c599ea 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/SettlementReportsClient.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/Clients/Wholesale/SettlementReports/SettlementReportsClient.cs @@ -67,15 +67,19 @@ public async Task> GetAsync(Cancellati return actualResponseContent.Concat(actualResponseApiContent).OrderByDescending(x => x.CreatedDateTime); } - public async Task DownloadAsync(SettlementReportRequestId requestId, CancellationToken cancellationToken) + public async Task DownloadAsync(SettlementReportRequestId requestId, bool fromApi, CancellationToken cancellationToken) { + using var requestApi = new HttpRequestMessage(HttpMethod.Get, "settlement-reports/download"); using var request = new HttpRequestMessage(HttpMethod.Post, "api/SettlementReportDownload"); request.Content = new StringContent( JsonConvert.SerializeObject(requestId), Encoding.UTF8, "application/json"); - var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + var response = await (fromApi + ? _apiHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + : _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)); + response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(cancellationToken); diff --git a/apps/dh/api-dh/source/DataHub.WebApi/Controllers/WholesaleSettlementReportController.cs b/apps/dh/api-dh/source/DataHub.WebApi/Controllers/WholesaleSettlementReportController.cs index 4d141f95f2..f22b4cb0e3 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/Controllers/WholesaleSettlementReportController.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/Controllers/WholesaleSettlementReportController.cs @@ -34,9 +34,9 @@ public WholesaleSettlementReportController( [HttpGet("DownloadReport")] [Produces("application/zip")] - public async Task> DownloadReportAsync([FromQuery] string settlementReportId) + public async Task> DownloadReportAsync([FromQuery] string settlementReportId, [FromQuery] bool fromApi) { - var reportStream = await _settlementReportsClient.DownloadAsync(new SettlementReportRequestId(settlementReportId), default); + var reportStream = await _settlementReportsClient.DownloadAsync(new SettlementReportRequestId(settlementReportId), fromApi, default); var fileName = "SettlementReport.zip"; return File(reportStream, MediaTypeNames.Application.Zip, fileName); } diff --git a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Query/SettlementReportsQuery.cs b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Query/SettlementReportsQuery.cs index 6139a32e1d..cd35eef602 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Query/SettlementReportsQuery.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Query/SettlementReportsQuery.cs @@ -46,20 +46,20 @@ public async Task> GetSettlementReportsAsync( }; settlementReports.Add(new SettlementReport( - report.RequestId.Id, - actor, - report.CalculationType, - new Interval(report.PeriodStart.ToInstant(), report.PeriodEnd.ToInstant()), - report.GridAreaCount, - report.ContainsBasisData, - string.Empty, - report.Progress, - settlementReportStatusType, - new Interval(Instant.FromDateTimeOffset(report.CreatedDateTime), report.EndedDateTime != null ? Instant.FromDateTimeOffset(report.EndedDateTime.Value) : null), - report.JobId is not null, - report.SplitReportPerGridArea, - report.IncludeMonthlyAmount, - report.GridAreas.Select(ga => ga.Key).ToArray())); + Id: report.RequestId.Id, + Actor: actor, + CalculationType: report.CalculationType, + Period: new Interval(report.PeriodStart.ToInstant(), report.PeriodEnd.ToInstant()), + NumberOfGridAreasInReport: report.GridAreaCount, + IncludesBasisData: report.ContainsBasisData, + StatusMessage: string.Empty, + Progress: report.Progress, + StatusType: settlementReportStatusType, + ExecutionTime: new Interval(Instant.FromDateTimeOffset(report.CreatedDateTime), report.EndedDateTime != null ? Instant.FromDateTimeOffset(report.EndedDateTime.Value) : null), + FromApi: report.JobId is not null, + CombineResultInASingleFile: !report.SplitReportPerGridArea, + IncludeMonthlyAmount: report.IncludeMonthlyAmount, + GridAreas: report.GridAreas.Select(ga => ga.Key).ToArray())); } return settlementReports; diff --git a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Resolvers/WholesaleResolvers.cs b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Resolvers/WholesaleResolvers.cs index bf9dab73d7..57ffdbe2a5 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Resolvers/WholesaleResolvers.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Resolvers/WholesaleResolvers.cs @@ -46,5 +46,5 @@ public async Task> GetGridAreasAsync( httpContextAccessor.HttpContext!, "DownloadReport", "WholesaleSettlementReport", - new { settlementReportId = result.Id }); + new { settlementReportId = result.Id, result.FromApi }); } diff --git a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Types/SettlementReports/SettlementReport.cs b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Types/SettlementReports/SettlementReport.cs index a31fd3d8a2..52d844d0a0 100644 --- a/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Types/SettlementReports/SettlementReport.cs +++ b/apps/dh/api-dh/source/DataHub.WebApi/GraphQL/Types/SettlementReports/SettlementReport.cs @@ -30,6 +30,6 @@ public sealed record SettlementReport( SettlementReportStatusType StatusType, Interval ExecutionTime, bool FromApi, - bool SplitReportPerGridArea, + bool CombineResultInASingleFile, bool IncludeMonthlyAmount, string[] GridAreas); diff --git a/bun.lockb b/bun.lockb index 191aab9cde..7a3ab938dc 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/libs/dh/globalization/assets-localization/src/assets/i18n/da.json b/libs/dh/globalization/assets-localization/src/assets/i18n/da.json index 33b5acf8c4..384e436764 100644 --- a/libs/dh/globalization/assets-localization/src/assets/i18n/da.json +++ b/libs/dh/globalization/assets-localization/src/assets/i18n/da.json @@ -31,6 +31,7 @@ "p-001": "produktion" }, "duration": { + "seconds": "{{ seconds }}s", "minutes": "{{ minutes }}m", "hours": "{{ hours }}t {{ duration.minutes }}", "days": "{{ days }}d {{ duration.hours }}" @@ -1127,7 +1128,14 @@ "numberOfGridAreasInReport": "Netområder", "basisData": "Datagrundlag", "status": "Status", - "actorName": "Aktørnavn" + "actorName": "Aktør" + }, + "noData": "Ingen data", + "gridAreasAndCount": "{{ gridAreas }}, +{{ remainingGridAreasCount }} flere", + "drawer": { + "monthlySum": "Månedssummer", + "combined": "Kombineret", + "gridAreas": "Netområder" }, "reportStatus": { "IN_PROGRESS": "I gang ({{ progress }}%)", diff --git a/libs/dh/globalization/assets-localization/src/assets/i18n/en.json b/libs/dh/globalization/assets-localization/src/assets/i18n/en.json index f8da986768..ebce56f3a4 100644 --- a/libs/dh/globalization/assets-localization/src/assets/i18n/en.json +++ b/libs/dh/globalization/assets-localization/src/assets/i18n/en.json @@ -31,6 +31,7 @@ "p-001": "production" }, "duration": { + "seconds": "{{ seconds }}s", "minutes": "{{ minutes }}m", "hours": "{{ hours }}h {{ duration.minutes }}", "days": "{{ days }}d {{ duration.hours }}" @@ -1130,6 +1131,13 @@ "status": "Status", "actorName": "Market participant" }, + "noData": "No data", + "gridAreasAndCount": "{{ gridAreas }}, +{{ remainingGridAreasCount }} more", + "drawer": { + "monthlySum": "Monthly sum", + "combined": "Kombineret", + "gridAreas": "Grid areas" + }, "reportStatus": { "IN_PROGRESS": "In progress ({{ progress }}%)", "ERROR": "Error", diff --git a/libs/dh/market-participant/actors/feature-actors/src/lib/drawer/dh-actor-drawer.component.html b/libs/dh/market-participant/actors/feature-actors/src/lib/drawer/dh-actor-drawer.component.html index 2254aec738..2ae5a10d15 100644 --- a/libs/dh/market-participant/actors/feature-actors/src/lib/drawer/dh-actor-drawer.component.html +++ b/libs/dh/market-participant/actors/feature-actors/src/lib/drawer/dh-actor-drawer.component.html @@ -122,7 +122,7 @@

} - + {{ organization?.name }}

} - + diff --git a/libs/dh/shared/data-access-mocks/src/lib/data/wholesale-settlement-reports.ts b/libs/dh/shared/data-access-mocks/src/lib/data/wholesale-settlement-reports.ts index 06766659f6..cf46f1047d 100644 --- a/libs/dh/shared/data-access-mocks/src/lib/data/wholesale-settlement-reports.ts +++ b/libs/dh/shared/data-access-mocks/src/lib/data/wholesale-settlement-reports.ts @@ -26,8 +26,9 @@ const periodEnd = new Date('2021-12-02T23:00:00Z'); const executionTimeStart = new Date('2024-09-11T06:12:00Z'); const executionTimeEnd_Days = new Date('2024-09-12T17:15:00Z'); -const executionTimeStart_Hours = new Date('2024-09-11T14:44:00Z'); -const executionTimeStart_Minutes = new Date('2024-09-11T06:48:00Z'); +const executionTimeEnd_Hours = new Date('2024-09-11T14:44:00Z'); +const executionTimeEnd_Minutes = new Date('2024-09-11T06:48:00Z'); +const executionTimeEnd_Seconds = new Date('2024-09-11T06:12:42Z'); export const wholesaleSettlementReportsQueryMock = ( apiBase: string @@ -54,6 +55,9 @@ export const wholesaleSettlementReportsQueryMock = ( }, settlementReportDownloadUrl: `${apiBase}/v1/WholesaleSettlementReport/DownloadReport`, fromApi: true, + includeMonthlyAmount: true, + combineResultInASingleFile: true, + gridAreas: ['003', '004', '005', '006', '007'], }, { __typename: 'SettlementReport', @@ -71,17 +75,20 @@ export const wholesaleSettlementReportsQueryMock = ( }, executionTime: { start: executionTimeStart, - end: executionTimeStart_Hours, + end: executionTimeEnd_Hours, }, settlementReportDownloadUrl: `${apiBase}/v1/WholesaleSettlementReport/DownloadReport`, fromApi: true, + includeMonthlyAmount: false, + combineResultInASingleFile: true, + gridAreas: ['100', '101', '102'], }, { __typename: 'SettlementReport', id: '85d1798474654be1b8c2f5bc543ed333', calculationType: CalculationType.WholesaleFixing, period: { start: periodStart, end: periodEnd }, - numberOfGridAreasInReport: 3, + numberOfGridAreasInReport: 0, includesBasisData: true, statusType: SettlementReportStatusType.Error, progress: 75, @@ -92,14 +99,60 @@ export const wholesaleSettlementReportsQueryMock = ( }, executionTime: { start: executionTimeStart, - end: executionTimeStart_Minutes, + end: executionTimeEnd_Minutes, }, settlementReportDownloadUrl: `${apiBase}/v1/WholesaleSettlementReport/DownloadReport`, fromApi: true, + includeMonthlyAmount: true, + combineResultInASingleFile: false, + gridAreas: [], }, { __typename: 'SettlementReport', id: '85d1798474654be1b8c2f5bc543ed444', + calculationType: CalculationType.ThirdCorrectionSettlement, + period: { start: periodStart, end: periodEnd }, + numberOfGridAreasInReport: 15, + includesBasisData: false, + statusType: SettlementReportStatusType.Error, + progress: 50, + actor: { + __typename: 'Actor', + id: '3', + name: 'Blå Strøm', + }, + executionTime: { + start: executionTimeStart, + end: executionTimeEnd_Seconds, + }, + settlementReportDownloadUrl: `${apiBase}/v1/WholesaleSettlementReport/DownloadReport`, + fromApi: true, + includeMonthlyAmount: false, + combineResultInASingleFile: false, + gridAreas: [ + '700', + '701', + '702', + '703', + '704', + '705', + '706', + '707', + '708', + '709', + '710', + '711', + '712', + '713', + '714', + '715', + '716', + '718', + ], + }, + { + __typename: 'SettlementReport', + id: '85d1798474654be1b8c2f5bc543ed555', calculationType: CalculationType.FirstCorrectionSettlement, period: { start: periodStart, end: null }, numberOfGridAreasInReport: 42, @@ -117,6 +170,9 @@ export const wholesaleSettlementReportsQueryMock = ( }, settlementReportDownloadUrl: `${apiBase}/v1/WholesaleSettlementReport/DownloadReport`, fromApi: true, + includeMonthlyAmount: true, + combineResultInASingleFile: true, + gridAreas: [], }, ], }); diff --git a/libs/dh/shared/domain/src/lib/generated/v1/api/wholesale-settlement-report-http.service.ts b/libs/dh/shared/domain/src/lib/generated/v1/api/wholesale-settlement-report-http.service.ts index de179a207b..22399fd4d6 100644 --- a/libs/dh/shared/domain/src/lib/generated/v1/api/wholesale-settlement-report-http.service.ts +++ b/libs/dh/shared/domain/src/lib/generated/v1/api/wholesale-settlement-report-http.service.ts @@ -93,19 +93,24 @@ export class WholesaleSettlementReportHttp { /** * @param settlementReportId + * @param fromApi * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable; - public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable>; - public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable>; - public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable { + public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, fromApi?: boolean, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable; + public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, fromApi?: boolean, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable>; + public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, fromApi?: boolean, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable>; + public v1WholesaleSettlementReportDownloadReportGet(settlementReportId?: string, fromApi?: boolean, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/zip', context?: HttpContext, transferCache?: boolean}): Observable { let localVarQueryParameters = new HttpParams({encoder: this.encoder}); if (settlementReportId !== undefined && settlementReportId !== null) { localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, settlementReportId, 'settlementReportId'); } + if (fromApi !== undefined && fromApi !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + fromApi, 'fromApi'); + } let localVarHeaders = this.defaultHeaders; diff --git a/libs/dh/wholesale/data-access-graphql/query/get-settlement-reports.graphql b/libs/dh/wholesale/data-access-graphql/query/get-settlement-reports.graphql index ede22aa286..e45611b076 100644 --- a/libs/dh/wholesale/data-access-graphql/query/get-settlement-reports.graphql +++ b/libs/dh/wholesale/data-access-graphql/query/get-settlement-reports.graphql @@ -14,5 +14,8 @@ query getSettlementReports { settlementReportDownloadUrl executionTime fromApi + combineResultInASingleFile + includeMonthlyAmount + gridAreas } } diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.html b/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.html new file mode 100644 index 0000000000..3d0a1572b5 --- /dev/null +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.html @@ -0,0 +1,113 @@ + +@let reportView = report(); + + + + @if (reportView && reportView.statusType !== "COMPLETED") { + + } + + + + @if (reportView) { +

{{ t("calculationTypes." + reportView?.calculationType) }}

+ + + } +
+ + @if (reportView?.statusType === "COMPLETED") { + + {{ "wholesale.settlementReports.reportStatus.download" | transloco }} + + } + + @if (drawer.isOpen && reportView) { + + + + + +

{{ t("drawer.gridAreas") }}

+ {{ tableSource.data.length }} +
+ + @if (tableSource.data.length > 0) { + + } @else { + {{ t("noData") }} + } +
+
+ } +
diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.ts b/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.ts new file mode 100644 index 0000000000..70ca413ff3 --- /dev/null +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/drawer/dh-settlement-report-drawer.component.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, effect, input, output, viewChild } from '@angular/core'; +import { TranslocoDirective, TranslocoPipe } from '@ngneat/transloco'; + +import { WATT_DRAWER, WattDrawerComponent } from '@energinet-datahub/watt/drawer'; +import { WattDatePipe } from '@energinet-datahub/watt/date'; +import { WattButtonComponent } from '@energinet-datahub/watt/button'; +import { + WattDescriptionListComponent, + WattDescriptionListItemComponent, +} from '@energinet-datahub/watt/description-list'; +import { WATT_CARD } from '@energinet-datahub/watt/card'; +import { WATT_TABLE, WattTableColumnDef, WattTableDataSource } from '@energinet-datahub/watt/table'; +import { VaterStackComponent } from '@energinet-datahub/watt/vater'; + +import { DhDurationComponent } from '../util/dh-duration.component'; +import { DhSettlementReport } from '../dh-settlement-report'; +import { DhSettlementReportsStatusComponent } from '../util/dh-settlement-reports-status.component'; + +@Component({ + selector: 'dh-settlement-report-drawer', + standalone: true, + imports: [ + TranslocoPipe, + TranslocoDirective, + + WATT_CARD, + WATT_TABLE, + WATT_DRAWER, + WattDatePipe, + VaterStackComponent, + WattButtonComponent, + WattDescriptionListComponent, + WattDescriptionListItemComponent, + + DhDurationComponent, + DhSettlementReportsStatusComponent, + ], + styles: [ + ` + :host { + display: block; + } + + .report-heading { + margin: 0; + margin-bottom: var(--watt-space-s); + } + + .metadata { + display: flex; + gap: var(--watt-space-ml); + } + + .metadata__item { + align-items: center; + display: flex; + gap: var(--watt-space-s); + } + + .card-metadata { + margin: var(--watt-space-ml); + width: 60%; + } + + .card-grid-areas { + margin: var(--watt-space-ml); + } + `, + ], + templateUrl: './dh-settlement-report-drawer.component.html', +}) +export class DhSettlementReportDrawerComponent { + drawer = viewChild.required(WattDrawerComponent); + + tableSource = new WattTableDataSource(); + + columns: WattTableColumnDef = { + code: { accessor: (value) => value }, + }; + + report = input(); + + closed = output(); + download = output(); + + constructor() { + effect(() => { + if (this.report()) { + this.drawer().open(); + this.tableSource.data = this.report()?.gridAreas ?? []; + } else { + this.drawer().close(); + } + }); + } +} diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/modal/dh-request-settlement-report-modal.component.ts b/libs/dh/wholesale/feature-settlement-reports/src/lib/modal/dh-request-settlement-report-modal.component.ts index b452c18336..77d3a4c383 100644 --- a/libs/dh/wholesale/feature-settlement-reports/src/lib/modal/dh-request-settlement-report-modal.component.ts +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/modal/dh-request-settlement-report-modal.component.ts @@ -306,7 +306,7 @@ export class DhRequestSettlementReportModalComponent extends WattTypedModal { preventLargeTextFiles: !allowLargeTextFiles, energySupplier: energySupplier == ALL_ENERGY_SUPPLIERS ? null : energySupplier, csvLanguage: translate('selectedLanguageIso'), - useApi: true, + useApi: false, }, }, refetchQueries: (result) => { diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.html b/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.html index d452c20dd4..b71f735e6c 100644 --- a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.html +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.html @@ -19,17 +19,13 @@ [dataSource]="tableDataSource" [columns]="columns" [displayedColumns]="displayedColumns" + [activeRow]="activeRow()" + (rowClick)="onRowClick($event)" > {{ entry.executionTime.start | wattDate: "long" }} - - - - {{ entry.actor?.name }} @@ -51,16 +47,25 @@ let entry " > - {{ entry.numberOfGridAreasInReport }} - + @let gridAreas = entry.gridAreas; - - @if (entry.includesBasisData) { - {{ "yes" | transloco }} + @if (gridAreas.length > 0) { + @if (gridAreas.length < 4) { + {{ gridAreas.join(", ") }} + } @else { + {{ + t("gridAreasAndCount", { + gridAreas: gridAreas.slice(0, 2).join(", "), + remainingGridAreasCount: gridAreas.length - 2, + }) + }} + } } @else { - {{ "no" | transloco }} + @if (entry.numberOfGridAreasInReport > 0) { + [{{ entry.numberOfGridAreasInReport }}] + } @else { + {{ t("noData") }} + } } @@ -68,8 +73,14 @@ + + diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.ts b/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.ts index 568c394e7b..93f22d3251 100644 --- a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.ts +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-table.component.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, effect, inject, input } from '@angular/core'; +import { Component, effect, inject, input, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { switchMap } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -31,8 +31,9 @@ import { streamToFile } from '@energinet-datahub/dh/shared/ui-util'; import { PermissionService } from '@energinet-datahub/dh/shared/feature-authorization'; import { DhSettlementReport, DhSettlementReports } from '../dh-settlement-report'; -import { DhSettlementReportsStatusComponent } from './dh-settlement-reports-status.component'; -import { DhDurationComponent } from './dh-duration.component'; +import { DhSettlementReportsStatusComponent } from '../util/dh-settlement-reports-status.component'; +import { DhDurationComponent } from '../util/dh-duration.component'; +import { DhSettlementReportDrawerComponent } from '../drawer/dh-settlement-report-drawer.component'; @Component({ selector: 'dh-settlement-reports-table', @@ -57,6 +58,7 @@ import { DhDurationComponent } from './dh-duration.component'; VaterStackComponent, DhSettlementReportsStatusComponent, + DhSettlementReportDrawerComponent, DhDurationComponent, ], }) @@ -67,12 +69,10 @@ export class DhSettlementReportsTableComponent { columns: WattTableColumnDef = { startedAt: { accessor: (report) => report.executionTime.start }, - executionTime: { accessor: null }, actorName: { accessor: (report) => report.actor?.name }, calculationType: { accessor: 'calculationType' }, period: { accessor: (report) => report.period.start }, numberOfGridAreasInReport: { accessor: 'numberOfGridAreasInReport' }, - includesBasisData: { accessor: 'includesBasisData' }, status: { accessor: 'statusType' }, }; @@ -82,6 +82,8 @@ export class DhSettlementReportsTableComponent { settlementReports = input.required(); + activeRow = signal(undefined); + constructor() { this.permissionService .isFas() @@ -95,7 +97,15 @@ export class DhSettlementReportsTableComponent { effect(() => (this.tableDataSource.data = this.settlementReports())); } - downloadReport(settlementReport: DhSettlementReport) { + onRowClick(settlementReport: DhSettlementReport): void { + this.activeRow.set(settlementReport); + } + + downloadReport(event: Event, settlementReport: DhSettlementReport): void { + // Prevent the row click event from firing + // so the drawer doesn't open + event.stopPropagation(); + const { settlementReportDownloadUrl } = settlementReport; if (!settlementReportDownloadUrl) { diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-duration.component.ts b/libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-duration.component.ts similarity index 87% rename from libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-duration.component.ts rename to libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-duration.component.ts index c8016033c8..990c4abc3a 100644 --- a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-duration.component.ts +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-duration.component.ts @@ -35,6 +35,8 @@ import { DhEmDashFallbackPipe } from '@energinet-datahub/dh/shared/ui-util'; {{ 'duration.hours' | transloco: durationView }} } @else if (durationView.minutes > 0) { {{ 'duration.minutes' | transloco: durationView }} + } @else if (durationView.seconds > 0) { + {{ 'duration.seconds' | transloco: durationView }} } `, }) @@ -51,6 +53,11 @@ export class DhDurationComponent { const diffInMilliseconds = dayjs(end).diff(start, 'milliseconds'); const duration = dayjs.duration(diffInMilliseconds); - return { days: duration.days(), hours: duration.hours(), minutes: duration.minutes() }; + return { + days: duration.days(), + hours: duration.hours(), + minutes: duration.minutes(), + seconds: duration.seconds(), + }; }); } diff --git a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-status.component.ts b/libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-settlement-reports-status.component.ts similarity index 88% rename from libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-status.component.ts rename to libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-settlement-reports-status.component.ts index 04cf94f4ed..6de1607d2f 100644 --- a/libs/dh/wholesale/feature-settlement-reports/src/lib/table/dh-settlement-reports-status.component.ts +++ b/libs/dh/wholesale/feature-settlement-reports/src/lib/util/dh-settlement-reports-status.component.ts @@ -36,9 +36,13 @@ import { WattButtonComponent } from '@energinet-datahub/watt/button'; {{ t(status()) }} } @case ('COMPLETED') { - {{ - t('download') - }} + {{ t('download') }} } } `, @@ -47,5 +51,6 @@ import { WattButtonComponent } from '@energinet-datahub/watt/button'; export class DhSettlementReportsStatusComponent { status = input.required(); progress = input.required(); - download = output(); + + download = output(); } diff --git a/libs/eo/auth/data-access/auth.service.ts b/libs/eo/auth/data-access/auth.service.ts index db0f5c3545..585d6f7eb9 100644 --- a/libs/eo/auth/data-access/auth.service.ts +++ b/libs/eo/auth/data-access/auth.service.ts @@ -19,6 +19,8 @@ import { Injectable, inject, signal } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; import { User, UserManager } from 'oidc-client-ts'; +import { WindowService } from '@energinet-datahub/gf/util-browser'; + import { eoB2cEnvironmentToken, EoB2cEnvironment, @@ -42,6 +44,7 @@ export interface EoUser { export class EoAuthService { private transloco = inject(TranslocoService); private http: HttpClient = inject(HttpClient); + private window = inject(WindowService).nativeWindow; private b2cEnvironment: EoB2cEnvironment = inject(eoB2cEnvironmentToken); private apiEnvironment: EoApiEnvironment = inject(eoApiEnvironmentToken); private userManager: UserManager | null = null; @@ -49,6 +52,8 @@ export class EoAuthService { user = signal(null); constructor() { + if (!this.window) return; + const settings = { /* * The authority is the URL of the OIDC provider. diff --git a/libs/eo/core/shell/src/lib/eo-shell.component.ts b/libs/eo/core/shell/src/lib/eo-shell.component.ts index c9b33f1572..4302efc16a 100644 --- a/libs/eo/core/shell/src/lib/eo-shell.component.ts +++ b/libs/eo/core/shell/src/lib/eo-shell.component.ts @@ -15,14 +15,26 @@ * limitations under the License. */ import { NgIf } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnDestroy, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; import { RouterModule } from '@angular/router'; import { Title } from '@angular/platform-browser'; -import { TranslocoPipe } from '@ngneat/transloco'; +import { TranslocoPipe, TranslocoService } from '@ngneat/transloco'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { WattShellComponent } from '@energinet-datahub/watt/shell'; import { WattButtonComponent } from '@energinet-datahub/watt/button'; import { VaterSpacerComponent, VaterStackComponent } from '@energinet-datahub/watt/vater'; +import { + CookieInformationService, + CookieInformationCulture, +} from '@energinet-datahub/gf/util-cookie-information'; import { translations } from '@energinet-datahub/eo/translations'; import { EoLanguageSwitcherComponent } from '@energinet-datahub/eo/globalization/feature-language-switcher'; @@ -152,25 +164,35 @@ import { EoAccountMenuComponent } from './eo-account-menu'; `, }) -export class EoShellComponent implements OnDestroy { +export class EoShellComponent implements OnInit, OnDestroy { protected titleService = inject(Title); private idleTimerService = inject(IdleTimerService); private authService = inject(EoAuthService); + private transloco = inject(TranslocoService); + private destroyRef = inject(DestroyRef); + private cookieInformationService: CookieInformationService = inject(CookieInformationService); protected translations = translations; protected cookiesSet: string | null = null; constructor() { this.idleTimerService.startMonitor(); - this.getBannerStatus(); } - onLogout() { - this.authService.logout(); + ngOnInit(): void { + this.cookieInformationService.init({ + culture: this.transloco.getActiveLang() as CookieInformationCulture, + }); + + this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => { + this.cookieInformationService.reInit({ + culture: lang as CookieInformationCulture, + }); + }); } - getBannerStatus() { - this.cookiesSet = localStorage.getItem('cookiesAccepted'); + onLogout() { + this.authService.logout(); } ngOnDestroy() { diff --git a/libs/eo/landing-page/shell/src/lib/shell.component.ts b/libs/eo/landing-page/shell/src/lib/shell.component.ts index d25b2b506a..b2c3645a56 100644 --- a/libs/eo/landing-page/shell/src/lib/shell.component.ts +++ b/libs/eo/landing-page/shell/src/lib/shell.component.ts @@ -14,7 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { + CookieInformationService, + CookieInformationCulture, +} from '@energinet-datahub/gf/util-cookie-information'; import { EoLandingPageHeaderComponent } from './header.component'; import { EoLandingPageHeroComponent } from './hero.component'; @@ -112,4 +119,20 @@ import { EoLandingPageNamingComponent } from './naming.component'; } `, }) -export class EoLandingPageShellComponent {} +export class EoLandingPageShellComponent implements OnInit { + private transloco = inject(TranslocoService); + private cookieInformationService: CookieInformationService = inject(CookieInformationService); + private destroyRef = inject(DestroyRef); + + ngOnInit(): void { + this.cookieInformationService.init({ + culture: this.transloco.getActiveLang() as CookieInformationCulture, + }); + + this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => { + this.cookieInformationService.reInit({ + culture: lang as CookieInformationCulture, + }); + }); + } +} diff --git a/libs/gf/util-browser/src/index.ts b/libs/gf/util-browser/src/index.ts index 4bb8c85d73..3b6c46746b 100644 --- a/libs/gf/util-browser/src/index.ts +++ b/libs/gf/util-browser/src/index.ts @@ -15,3 +15,4 @@ * limitations under the License. */ export * from './lib/gf-browser-configuration.provider'; +export * from './lib/window.service'; diff --git a/libs/gf/util-browser/src/lib/window.service.ts b/libs/gf/util-browser/src/lib/window.service.ts new file mode 100644 index 0000000000..431a0c7415 --- /dev/null +++ b/libs/gf/util-browser/src/lib/window.service.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable, PLATFORM_ID, inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; + +@Injectable({ + providedIn: 'root', +}) +export class WindowService { + private platformId = inject(PLATFORM_ID); + + get nativeWindow(): Window | null { + if (isPlatformBrowser(this.platformId)) { + return window; + } + return null; + } +} diff --git a/libs/gf/util-cookie-information/.eslintrc.json b/libs/gf/util-cookie-information/.eslintrc.json new file mode 100644 index 0000000000..d2ebb714e5 --- /dev/null +++ b/libs/gf/util-cookie-information/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "gf", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "gf", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/gf/util-cookie-information/README.md b/libs/gf/util-cookie-information/README.md new file mode 100644 index 0000000000..136fac5cee --- /dev/null +++ b/libs/gf/util-cookie-information/README.md @@ -0,0 +1,54 @@ +# Cookie information utilities + +This service provides an easy way to integrate the Cookie Information consent banner. +> *documentation details of cookie information itself can be found [here](https://support.cookieinformation.com/en/).* + +**🚨 Please notice:** It currently only supports English (en) and Danish (da) languages. To add another language, read [here](#add-support-for-other-languages). + +**🚨 Please notice:** [Cookie information does **NOT** support testing the solution on localhost](https://support.cookieinformation.com/en/articles/6718369-technical-faq#h_37636a716d). + +## Getting started + +Before you can use cookie information, you will need someone to add your domain(s) at cookieinformation. + +## Usage + +```ts +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; + +import { CookieInformationService } from '@energinet-datahub/gf/util-cookie-information'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule, RouterOutlet], + template: '' +}) +export class AppComponent implements OnInit { + private cookieInformationService: CookieInformationService = inject(CookieInformationService); + private transloco = inject(TranslocoService); + private destroyRef = inject(DestroyRef); + + ngOnInit(): void { + // Initialize cookie information + this.cookieInformationService.init({ + culture: this.transloco.getActiveLang() as CookieInformationCulture, + }); + + // Reload cookie information on language changes + this.transloco.langChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => { + this.cookieInformationService.reInit({ + culture: lang as CookieInformationCulture, + }); + }); + } +} +``` + +## Add support for other languages + +Add the culture to `src/lib/supported-cultures.ts`. + +>**Note:** available cultures can be found [here](https://support.cookieinformation.com/en/articles/5444177-pop-up-implementation#h_8e9379fa2f). diff --git a/libs/gf/util-cookie-information/jest.config.ts b/libs/gf/util-cookie-information/jest.config.ts new file mode 100644 index 0000000000..b028dd4f58 --- /dev/null +++ b/libs/gf/util-cookie-information/jest.config.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable */ +export default { + displayName: 'gf-util-browser', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: {}, + coverageDirectory: '../../../coverage/libs/gf/util-browser', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/gf/util-cookie-information/project.json b/libs/gf/util-cookie-information/project.json new file mode 100644 index 0000000000..190a84d33a --- /dev/null +++ b/libs/gf/util-cookie-information/project.json @@ -0,0 +1,21 @@ +{ + "name": "gf-util-cookie-information", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/gf/util-cookie-information/src", + "prefix": "gf", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/gf/util-cookie-information"], + "options": { + "jestConfig": "libs/gf/util-cookie-information/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + }, + "tags": ["product:gf", "domain:shared", "type:util"] +} diff --git a/libs/gf/util-cookie-information/src/index.ts b/libs/gf/util-cookie-information/src/index.ts new file mode 100644 index 0000000000..7ae0edd3ac --- /dev/null +++ b/libs/gf/util-cookie-information/src/index.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './lib/cookie-information.service'; +export * from './lib/supported-cultures'; diff --git a/libs/gf/util-cookie-information/src/lib/cookie-information.service.spec.ts b/libs/gf/util-cookie-information/src/lib/cookie-information.service.spec.ts new file mode 100644 index 0000000000..ab5f9de0c8 --- /dev/null +++ b/libs/gf/util-cookie-information/src/lib/cookie-information.service.spec.ts @@ -0,0 +1,130 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { WindowService } from '@energinet-datahub/gf/util-browser'; +import { CookieInformationService } from './cookie-information.service'; +import { CookieInformationCulture } from './supported-cultures'; + +function setup(config?: {hostname?: string, ssr?: boolean}) { + const appendChildSpy = jest.fn(); + const removeChildSpy = jest.fn(); + const createElementSpy = jest.fn().mockImplementation(() => ({ + setAttribute: jest.fn(), + })); + const loadConsentSpy = jest.fn(); + + const mockDocument = { + body: { + appendChild: appendChildSpy, + removeChild: removeChildSpy, + } as unknown as HTMLElement, + createElement: createElementSpy, + getElementById: jest.fn(), + location: { + hostname: config?.hostname, + } as Location, + }; + + const mockWindowService = { + nativeWindow: config?.ssr ? undefined : { + CookieInformation: { + loadConsent: loadConsentSpy, + }, + } as unknown as Window, + }; + + TestBed.configureTestingModule({ + providers: [ + CookieInformationService, + { provide: DOCUMENT, useValue: mockDocument }, + { provide: WindowService, useValue: mockWindowService }, + ], + }); + const service = TestBed.inject(CookieInformationService); + + return({ + service, + mockDocument, + createElementSpy, + appendChildSpy, + removeChildSpy, + loadConsentSpy, + }); +} + +describe('CookieInformationService', () => { + it('should not add script when on localhost', () => { + const {mockDocument, service, createElementSpy} = setup({hostname: 'localhost'}); + + mockDocument.location.hostname = 'localhost'; + service.init({ culture: 'en' }); + + const createdScript = createElementSpy.mock.results; + expect(createdScript.length).toBe(0); + }); + + it.each(['en', 'da'])( + 'should add script to body with correct attributes for culture %s', + (culture) => { + const {service, createElementSpy, appendChildSpy} = setup(); + + service.init({ culture }); + + expect(createElementSpy).toHaveBeenCalledWith('script'); + expect(appendChildSpy).toHaveBeenCalledTimes(1); + + const createdScript = createElementSpy.mock.results[0].value; + expect(createdScript.id).toBe('CookieConsent'); + expect(createdScript.src).toBe('https://policy.app.cookieinformation.com/uc.js'); + expect(createdScript.setAttribute).toHaveBeenCalledWith( + 'data-culture', + culture.toUpperCase() + ); + expect(createdScript.setAttribute).toHaveBeenCalledWith('data-gcm-version', '2.0'); + expect(createdScript.type).toBe('text/javascript'); + + expect(appendChildSpy).toHaveBeenCalledWith(createdScript); + } + ); + + it('should remove existing script, add new one, and call loadConsent', () => { + const {service, mockDocument, createElementSpy, appendChildSpy, removeChildSpy, loadConsentSpy} = setup(); + const mockScript = { id: 'CookieConsent' } as HTMLElement; + mockDocument.getElementById.mockReturnValue(mockScript); + + service.reInit({ culture: 'da' }); + + expect(removeChildSpy).toHaveBeenCalledWith(mockScript); + expect(createElementSpy).toHaveBeenCalledWith('script'); + expect(appendChildSpy).toHaveBeenCalledTimes(1); + expect(loadConsentSpy).toHaveBeenCalled(); + + const createdScript = createElementSpy.mock.results[0].value; + expect(createdScript.setAttribute).toHaveBeenCalledWith('data-culture', 'DA'); + }); + + it('should not call loadConsent when window is undefined', () => { + const {service, mockDocument, loadConsentSpy} = setup({ssr: true}); + const mockScript = { id: 'CookieConsent' } as HTMLElement; + mockDocument.getElementById.mockReturnValue(mockScript); + + service.reInit({ culture: 'da' }); + + expect(loadConsentSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/gf/util-cookie-information/src/lib/cookie-information.service.ts b/libs/gf/util-cookie-information/src/lib/cookie-information.service.ts new file mode 100644 index 0000000000..51eebac04a --- /dev/null +++ b/libs/gf/util-cookie-information/src/lib/cookie-information.service.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable, inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { WindowService } from '@energinet-datahub/gf/util-browser'; + +import { CookieInformationCulture } from './supported-cultures'; + +export interface CookieInformationConfig { + culture: CookieInformationCulture; +} + +@Injectable({ + providedIn: 'root', +}) +export class CookieInformationService { + private document: Document = inject(DOCUMENT); + private window = inject(WindowService).nativeWindow; + + constructor() { + // Loading the cookie information is not supported on localhost: https://support.cookieinformation.com/en/articles/6718369-technical-faq#h_37636a716d + if (this.isLocalhost()) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + this.init = () => {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + this.reInit = () => {}; + } + } + + // Implementation details of cookie information can be found here: https://support.cookieinformation.com/en/articles/5444177-pop-up-implementation + init(config: CookieInformationConfig): void { + const { culture } = config; + + // Do not load the script if we are on localhost see: https://support.cookieinformation.com/en/articles/6718369-technical-faq#h_37636a716d + if (this.document.location.hostname === 'localhost') return; + this.addSciptToBody(culture); + } + + // This method is used to reinitialize the cookie information script, mostly used on language change + reInit(config: CookieInformationConfig): void { + this.removeScriptFromBody(); + this.init(config); + + if (!this.window) return; + + // Reload cookie information + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.window as any)['CookieInformation']?.loadConsent(); + } + + private isLocalhost(): boolean { + return this.document.location.hostname === 'localhost'; + } + + private addSciptToBody(culture: CookieInformationCulture): void { + const script = this.document.createElement('script'); + script.id = 'CookieConsent'; + script.src = 'https://policy.app.cookieinformation.com/uc.js'; + script.setAttribute('data-culture', culture.toUpperCase()); + script.setAttribute('data-gcm-version', '2.0'); + script.type = 'text/javascript'; + this.document.body.appendChild(script); + } + + private removeScriptFromBody(): void { + const script = this.document.getElementById('CookieConsent'); + if (script) { + this.document.body.removeChild(script); + } + } +} diff --git a/libs/gf/util-cookie-information/src/lib/supported-cultures.ts b/libs/gf/util-cookie-information/src/lib/supported-cultures.ts new file mode 100644 index 0000000000..8e3674c74f --- /dev/null +++ b/libs/gf/util-cookie-information/src/lib/supported-cultures.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type CookieInformationCulture = 'en' | 'da'; diff --git a/libs/gf/util-cookie-information/src/test-setup.ts b/libs/gf/util-cookie-information/src/test-setup.ts new file mode 100644 index 0000000000..066826eec5 --- /dev/null +++ b/libs/gf/util-cookie-information/src/test-setup.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2020 Energinet DataHub A/S + * + * Licensed under the Apache License, Version 2.0 (the "License2"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import 'jest-preset-angular/setup-jest'; diff --git a/libs/gf/util-cookie-information/tsconfig.json b/libs/gf/util-cookie-information/tsconfig.json new file mode 100644 index 0000000000..aeb1c9ace3 --- /dev/null +++ b/libs/gf/util-cookie-information/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "target": "es2020" + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/gf/util-cookie-information/tsconfig.lib.json b/libs/gf/util-cookie-information/tsconfig.lib.json new file mode 100644 index 0000000000..294f86ee71 --- /dev/null +++ b/libs/gf/util-cookie-information/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/gf/util-cookie-information/tsconfig.spec.json b/libs/gf/util-cookie-information/tsconfig.spec.json new file mode 100644 index 0000000000..a0be629167 --- /dev/null +++ b/libs/gf/util-cookie-information/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "target": "es2016" + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/package.json b/package.json index 500c26edf8..2f9215315d 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "date-fns": "2.29.2", "date-fns-tz": "2.0.0", "dayjs": "1.11.10", - "express": "4.20.0", + "express": "4.21.0", "graphql": "16.8.1", "graphql-sse": "^2.5.3", "highlight.js": "^11.9.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index 68a244cdd0..039835c39b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -390,6 +390,9 @@ "@energinet-datahub/gf/util-browser": [ "libs/gf/util-browser/src/index.ts" ], + "@energinet-datahub/gf/util-cookie-information": [ + "libs/gf/util-cookie-information/src/index.ts" + ], "@energinet-datahub/gf/util-msw": ["libs/gf/msw/util-msw/src/index.ts"], "@energinet-datahub/watt/badge": [ "libs/watt/src/lib/components/badge/index.ts"