From 17455a838dad8a1a09a7a38911a3eba8e8743042 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 5 Nov 2025 18:22:15 +0100 Subject: [PATCH] tried to fix timezone --- .../access-control-array-form/to-date.pipe.ts | 8 +-- src/app/shared/date-timezone.pipe.ts | 62 +++++++++++++++++++ src/app/shared/date.util.ts | 62 +++++++++++++++++++ src/app/shared/shared.module.ts | 2 + src/app/shared/timezone.service.ts | 52 ++++++++++++++++ .../metadata-information.component.html | 4 +- .../system-wide-alert-banner.component.ts | 10 +-- src/config/app-config.interface.ts | 2 + src/config/timezone-config.interface.ts | 10 +++ src/environments/environment.production.ts | 7 +++ src/environments/environment.ts | 7 +++ 11 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/app/shared/date-timezone.pipe.ts create mode 100644 src/app/shared/timezone.service.ts create mode 100644 src/config/timezone-config.interface.ts diff --git a/src/app/shared/access-control-form-container/access-control-array-form/to-date.pipe.ts b/src/app/shared/access-control-form-container/access-control-array-form/to-date.pipe.ts index 203d12a59d3..b91bb74e820 100644 --- a/src/app/shared/access-control-form-container/access-control-array-form/to-date.pipe.ts +++ b/src/app/shared/access-control-form-container/access-control-array-form/to-date.pipe.ts @@ -1,5 +1,6 @@ import {Pipe, PipeTransform} from '@angular/core'; import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap/datepicker/ngb-date-struct'; +import { dateToNgbDateStructInTimezone } from '../../date.util'; @Pipe({ // eslint-disable-next-line @angular-eslint/pipe-prefix @@ -13,11 +14,8 @@ export class ToDatePipe implements PipeTransform { } const date = new Date(dateValue); - return { - year: date.getFullYear(), - month: date.getMonth() + 1, - day: date.getDate() - } as NgbDateStruct; + // Use timezone-aware conversion to properly handle dates in Bratislava timezone + return dateToNgbDateStructInTimezone(date); } } diff --git a/src/app/shared/date-timezone.pipe.ts b/src/app/shared/date-timezone.pipe.ts new file mode 100644 index 00000000000..a0be1ab1532 --- /dev/null +++ b/src/app/shared/date-timezone.pipe.ts @@ -0,0 +1,62 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { formatInTimeZone } from 'date-fns-tz'; + +/** + * Custom date pipe that formats dates in the Bratislava timezone + */ +@Pipe({ + name: 'dateTimezone' +}) +export class DateTimezonePipe implements PipeTransform { + private datePipe = new DatePipe('en-US'); + private readonly timezone = 'Europe/Bratislava'; + + transform(value: any, format?: string, timezone?: string): string | null { + if (!value) { + return null; + } + + const tz = timezone || this.timezone; + const formatStr = format || 'medium'; + + try { + const date = new Date(value); + + // Handle common Angular date pipe formats + switch (formatStr) { + case 'short': + return formatInTimeZone(date, tz, 'd/M/yy, H:mm'); + case 'medium': + return formatInTimeZone(date, tz, 'd MMM y, H:mm:ss'); + case 'long': + return formatInTimeZone(date, tz, 'd MMMM y, H:mm:ss z'); + case 'full': + return formatInTimeZone(date, tz, 'EEEE, d MMMM y, H:mm:ss zzzz'); + case 'shortDate': + return formatInTimeZone(date, tz, 'd/M/yy'); + case 'mediumDate': + return formatInTimeZone(date, tz, 'd MMM y'); + case 'longDate': + return formatInTimeZone(date, tz, 'd MMMM y'); + case 'fullDate': + return formatInTimeZone(date, tz, 'EEEE, d MMMM y'); + case 'shortTime': + return formatInTimeZone(date, tz, 'H:mm'); + case 'mediumTime': + return formatInTimeZone(date, tz, 'H:mm:ss'); + case 'longTime': + return formatInTimeZone(date, tz, 'H:mm:ss z'); + case 'fullTime': + return formatInTimeZone(date, tz, 'H:mm:ss zzzz'); + default: + // Custom format string - use as is with date-fns-tz + return formatInTimeZone(date, tz, formatStr); + } + } catch (error) { + console.warn('DateTimezonePipe: Error formatting date', error); + // Fallback to standard Angular DatePipe + return this.datePipe.transform(value, format); + } + } +} \ No newline at end of file diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 5b74ed02d20..0099c13623c 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -4,6 +4,15 @@ import { isValid } from 'date-fns'; import isObject from 'lodash/isObject'; import { hasNoValue } from './empty.util'; +/** + * Get the configured display timezone from environment or fallback to browser timezone + */ +function getDisplayTimezone(): string { + // Fallback to Europe/Bratislava as default for this application + // In production, this should be injected via the TimezoneService + return 'Europe/Bratislava'; +} + /** * Returns true if the passed value is a NgbDateStruct. * @@ -33,6 +42,22 @@ export function dateToISOFormat(date: Date | NgbDateStruct | string): string { return formatInTimeZone(dateObj, 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'"); } +/** + * Returns a date formatted in the configured display timezone + * + * @param date The date to format + * @param format The format string (date-fns format) + * @param timezone Optional timezone override + * @return string the formatted date in the display timezone + */ +export function dateToDisplayFormat(date: Date | NgbDateStruct | string, format: string = "yyyy-MM-dd'T'HH:mm:ss", timezone?: string): string { + const dateObj: Date = (date instanceof Date) ? date : + ((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date)); + + const tz = timezone || getDisplayTimezone(); + return formatInTimeZone(dateObj, tz, format); +} + /** * Returns a Date object started from a NgbDateStruct object * @@ -77,6 +102,30 @@ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { }; } +/** + * Returns a NgbDateStruct object from a Date object using the display timezone + * + * @param date + * The Date to convert + * @return NgbDateStruct + * the NgbDateStruct object in display timezone + */ +export function dateToNgbDateStructInTimezone(date?: Date): NgbDateStruct { + if (hasNoValue(date)) { + date = new Date(); + } + + // Format the date in the display timezone and parse it back to get local components + const dateStr = formatInTimeZone(date, getDisplayTimezone(), 'yyyy-MM-dd'); + const [year, month, day] = dateStr.split('-').map(num => parseInt(num, 10)); + + return { + year, + month, + day + }; +} + /** * Returns a date in simplified format (YYYY-MM-DD). * @@ -90,6 +139,19 @@ export function dateToString(date: Date | NgbDateStruct): string { return formatInTimeZone(dateObj, 'UTC', 'yyyy-MM-dd'); } +/** + * Returns a date in simplified format (YYYY-MM-DD) in the display timezone. + * + * @param date + * The date to format + * @return string + * the formatted date in display timezone + */ +export function dateToStringInTimezone(date: Date | NgbDateStruct): string { + const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); + return formatInTimeZone(dateObj, getDisplayTimezone(), 'yyyy-MM-dd'); +} + /** * Checks if the given string represents a valid date * @param date the string to be checked diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c1f77f288b2..a8a8fbebdcb 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -28,6 +28,7 @@ import { FileSizePipe } from './utils/file-size-pipe'; import { MetadataFieldValidator } from './utils/metadatafield-validator.directive'; import { SafeUrlPipe } from './utils/safe-url-pipe'; import { ConsolePipe } from './utils/console.pipe'; +import { DateTimezonePipe } from './date-timezone.pipe'; import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component'; @@ -330,6 +331,7 @@ const PIPES = [ EnumKeysPipe, FileSizePipe, SafeUrlPipe, + DateTimezonePipe, TruncatePipe, EmphasizePipe, CapitalizePipe, diff --git a/src/app/shared/timezone.service.ts b/src/app/shared/timezone.service.ts new file mode 100644 index 00000000000..c107707b2d2 --- /dev/null +++ b/src/app/shared/timezone.service.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@angular/core'; +import { APP_CONFIG, AppConfig } from '../../config/app-config.interface'; + +/** + * Service for handling timezone operations and configuration + */ +@Injectable({ + providedIn: 'root' +}) +export class TimezoneService { + + constructor(@Inject(APP_CONFIG) private appConfig: AppConfig) {} + + /** + * Get the configured display timezone + * @returns string IANA timezone identifier + */ + getDisplayTimezone(): string { + return this.appConfig.timezone?.displayTimezone || this.getBrowserTimezone(); + } + + /** + * Get the browser's detected timezone + * @returns string IANA timezone identifier + */ + getBrowserTimezone(): string { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch (e) { + return 'UTC'; + } + } + + /** + * Get the timezone offset in minutes for a specific date and timezone + * @param date The date to get offset for + * @param timezone The timezone (defaults to display timezone) + * @returns number offset in minutes + */ + getTimezoneOffset(date: Date = new Date(), timezone?: string): number { + const tz = timezone || this.getDisplayTimezone(); + + try { + // Use Intl.DateTimeFormat to get timezone offset + const utcDate = new Date(date.toISOString().slice(0, -1) + 'Z'); + const tzDate = new Date(date.toLocaleString('en-US', { timeZone: tz })); + return (utcDate.getTime() - tzDate.getTime()) / (1000 * 60); + } catch (e) { + return 0; + } + } +} \ No newline at end of file diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html index 15dd4d72867..884602080e5 100644 --- a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html @@ -13,7 +13,7 @@

{{ 'submission.sections.sherpa.record.information.date.created' | translate }}

-

{{metadata.dateCreated | date: 'd MMMM Y H:mm:ss zzzz' }} +

{{metadata.dateCreated | dateTimezone: 'd MMMM Y H:mm:ss zzzz' }}

@@ -22,7 +22,7 @@

{{ 'submission.sections.sherpa.record.information.date.modified' | translate }}

-

{{metadata.dateModified| date: 'd MMMM Y H:mm:ss zzzz' }} +

{{metadata.dateModified | dateTimezone: 'd MMMM Y H:mm:ss zzzz' }}

diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index 27bdb14f1d7..54fd2a898a9 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -8,7 +8,7 @@ import { PaginatedList } from '../../core/data/paginated-list.model'; import { SystemWideAlert } from '../system-wide-alert.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BehaviorSubject, EMPTY, interval, Subscription } from 'rxjs'; -import { zonedTimeToUtc } from 'date-fns-tz'; +// Remove unused import as we now handle dates properly with native Date object import { isPlatformBrowser } from '@angular/common'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -67,7 +67,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { this.subscriptions.push(this.systemWideAlert$.pipe( switchMap((alert: SystemWideAlert) => { if (hasValue(alert) && hasValue(alert.countdownTo)) { - const date = zonedTimeToUtc(alert.countdownTo, 'UTC'); + const date = new Date(alert.countdownTo); const timeDifference = date.getTime() - new Date().getTime(); if (timeDifference > 0) { this.allocateTimeUnits(timeDifference); @@ -95,8 +95,10 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { * @param countdownTo - The date to count down to */ private setTimeDifference(countdownTo: string) { - const date = zonedTimeToUtc(countdownTo, 'UTC'); - + // The countdownTo string should be parsed as UTC time + const date = new Date(countdownTo); + + // Calculate difference with current time (browser time) const timeDifference = date.getTime() - new Date().getTime(); this.allocateTimeUnits(timeDifference); } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index e21ab6b303f..adffcaa5190 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -26,6 +26,7 @@ import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; import { SearchConfig } from './search-page-config.interface'; import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config'; import { MatomoConfig } from './matomo-config'; +import { TimezoneConfig } from './timezone-config.interface'; interface AppConfig extends Config { ui: UIServerConfig; @@ -57,6 +58,7 @@ interface AppConfig extends Config { accessibility: AccessibilitySettingsConfig; signpostingEnabled: boolean; matomo: MatomoConfig; + timezone?: TimezoneConfig; } /** diff --git a/src/config/timezone-config.interface.ts b/src/config/timezone-config.interface.ts new file mode 100644 index 00000000000..82399983aee --- /dev/null +++ b/src/config/timezone-config.interface.ts @@ -0,0 +1,10 @@ +/** + * Configuration for timezone settings + */ +export interface TimezoneConfig { + /** + * Default timezone for displaying dates and times + * Use IANA timezone identifiers (e.g., 'Europe/Bratislava', 'UTC', 'America/New_York') + */ + displayTimezone: string; +} \ No newline at end of file diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index e5066ab48fc..2c09b9c2fa0 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -33,4 +33,11 @@ export const environment: Partial = { enableSearchComponent: false, enableBrowseComponent: false, }, + + // Timezone settings + timezone: { + // Default timezone for displaying dates and times + // Use IANA timezone identifiers (e.g., 'Europe/Bratislava', 'UTC', 'America/New_York') + displayTimezone: 'Europe/Bratislava', + }, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 8be4ee7dbfe..00c500440d9 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -48,6 +48,13 @@ export const environment: Partial = { nameSpace: '/server', }, + // Timezone settings + timezone: { + // Default timezone for displaying dates and times + // Use IANA timezone identifiers (e.g., 'Europe/Bratislava', 'UTC', 'America/New_York') + displayTimezone: 'Europe/Bratislava', + }, + signpostingEnabled: false, };