From c3b723d083d60c2d04a142fc249fb2c6d9c80a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Erik=20St=C3=B8wer?= Date: Mon, 2 Dec 2024 14:54:08 +0100 Subject: [PATCH 1/2] fix: remove moment and date-fns libs, use @internationalized/date --- package-lock.json | 32 ++------- package.json | 3 +- .../BookingArrangementEditor/editor.tsx | 26 +++---- .../DayTypeAssignmentsEditor.tsx | 18 ++--- src/components/DurationPicker/index.tsx | 69 ++++++++++++++++--- .../LinesForExport/LinesForExport.test.tsx | 16 +++-- src/components/LinesForExport/index.tsx | 34 ++++----- .../ServiceJourneyEditor/CopyDialog.test.tsx | 18 ++--- .../ServiceJourneyEditor/CopyDialog.tsx | 59 ++++++++-------- src/custom.typings.d.ts | 2 +- src/helpers/dates.ts | 11 ++- src/helpers/validation.ts | 63 ++++++++++------- src/i18n/index.ts | 1 - src/model/DayTypeAssignment.ts | 6 +- src/utils/dates.ts | 59 ++++++++++++++++ 15 files changed, 261 insertions(+), 156 deletions(-) create mode 100644 src/utils/dates.ts diff --git a/package-lock.json b/package-lock.json index 496c7a738..25d0ce48d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,12 +29,12 @@ "@entur/tooltip": "5.1.3", "@entur/typography": "1.8.49", "@fintraffic/fds-coreui-css": "0.1.3", + "@internationalized/date": "^3.6.0", "@lit-labs/react": "2.1.3", "@reduxjs/toolkit": "1.9.7", "@sentry/react": "7.120.0", "axios": "1.7.8", "classnames": "2.5.1", - "date-fns": "2.30.0", "duration-fns": "3.0.2", "file-saver": "2.0.5", "graphql": "16.9.0", @@ -44,7 +44,6 @@ "lodash.clonedeep": "4.5.0", "lodash.isempty": "4.4.0", "lodash.isequal": "4.5.0", - "moment": "2.30.1", "oidc-client-ts": "3.1.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -1660,9 +1659,9 @@ } }, "node_modules/@internationalized/date": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz", - "integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz", + "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==", "dependencies": { "@swc/helpers": "^0.5.0" } @@ -4520,21 +4519,6 @@ "node": ">=18" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -6087,14 +6071,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/package.json b/package.json index 774ca98fe..6c82a6d07 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,12 @@ "@entur/tooltip": "5.1.3", "@entur/typography": "1.8.49", "@fintraffic/fds-coreui-css": "0.1.3", + "@internationalized/date": "^3.6.0", "@lit-labs/react": "2.1.3", "@reduxjs/toolkit": "1.9.7", "@sentry/react": "7.120.0", "axios": "1.7.8", "classnames": "2.5.1", - "date-fns": "2.30.0", "duration-fns": "3.0.2", "file-saver": "2.0.5", "graphql": "16.9.0", @@ -51,7 +51,6 @@ "lodash.clonedeep": "4.5.0", "lodash.isempty": "4.4.0", "lodash.isequal": "4.5.0", - "moment": "2.30.1", "oidc-client-ts": "3.1.0", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/src/components/BookingArrangementEditor/editor.tsx b/src/components/BookingArrangementEditor/editor.tsx index d49a1ef91..750bff898 100644 --- a/src/components/BookingArrangementEditor/editor.tsx +++ b/src/components/BookingArrangementEditor/editor.tsx @@ -8,9 +8,10 @@ import { Dropdown } from '@entur/dropdown'; import { Fieldset, Radio, RadioGroup, TextArea, TextField } from '@entur/form'; import { Label, LeadParagraph } from '@entur/typography'; import { TimeValue } from '@react-types/datepicker'; +import { CalendarDateTime } from '@internationalized/date'; import DurationPicker from 'components/DurationPicker'; import { TimeUnitPickerPosition } from 'components/TimeUnitPicker'; -import { format } from 'date-fns'; +import { getCurrentDateTime } from '../../utils/dates'; import { addOrRemove } from 'helpers/arrays'; import { getEnumInit, mapEnumToItems } from 'helpers/dropdown'; import BookingArrangement from 'model/BookingArrangement'; @@ -58,13 +59,15 @@ export default (props: Props) => { minimumBookingPeriod, } = bookingArrangement; - let latestbookingTimeAsDate: Date | undefined = undefined; + let latestbookingTimeAsDate: CalendarDateTime | undefined = undefined; if (latestBookingTime && latestBookingTime !== '') { - latestbookingTimeAsDate = new Date(); - latestbookingTimeAsDate.setHours(parseInt(latestBookingTime.split(':')[0])); - latestbookingTimeAsDate.setMinutes( - parseInt(latestBookingTime.split(':')[1]), - ); + const currentDateTime = getCurrentDateTime(); + const [hours, minutes] = latestBookingTime.split(':').map(Number); + latestbookingTimeAsDate = currentDateTime.copy(); + latestbookingTimeAsDate.set({ + hour: hours, + minute: minutes, + }); } const onContactChange = (contact: Contact) => @@ -249,16 +252,13 @@ export default (props: Props) => { locale={locale} disabled={bookingLimitType !== BOOKING_LIMIT_TYPE.TIME} selectedTime={ - latestbookingTimeAsDate - ? nativeDateToTimeValue(latestbookingTimeAsDate) - : null + latestbookingTimeAsDate ? latestbookingTimeAsDate : null } onChange={(date: TimeValue | null) => { let formattedDate; - const nativeDate = timeOrDateValueToNativeDate(date); - if (nativeDate != null) { - formattedDate = format(nativeDate, 'HH:mm'); + if (date != null) { + formattedDate = `${date.hour}:${date.minute}`; } onLatestBookingTimeChange(formattedDate); diff --git a/src/components/DayTypesEditor/DayTypeAssignmentsEditor.tsx b/src/components/DayTypesEditor/DayTypeAssignmentsEditor.tsx index ce1b56713..eb3609298 100644 --- a/src/components/DayTypesEditor/DayTypeAssignmentsEditor.tsx +++ b/src/components/DayTypesEditor/DayTypeAssignmentsEditor.tsx @@ -12,10 +12,10 @@ import { getErrorFeedback } from 'helpers/errorHandling'; import useUniqueKeys from 'hooks/useUniqueKeys'; import DayTypeAssignment from 'model/DayTypeAssignment'; import OperatingPeriod from 'model/OperatingPeriod'; -import moment from 'moment/moment'; -import React from 'react'; import { useIntl } from 'react-intl'; +import { getCurrentDate, calendarDateToISO } from '../../utils/dates'; import './styles.scss'; +import { parseAbsoluteToLocal } from '@internationalized/date'; type Props = { dayTypeAssignments: DayTypeAssignment[]; @@ -26,13 +26,13 @@ const DayTypeAssignmentsEditor = ({ dayTypeAssignments, onChange }: Props) => { const { formatMessage } = useIntl(); const addNewDayTypeAssignment = () => { - const today: string = moment().format('YYYY-MM-DD'); - const tomorrow: string = moment().add(1, 'days').format('YYYY-MM-DD'); + const today = getCurrentDate(); + const tomorrow = today.add({ days: 1 }); const dayTypeAssignment = { isAvailable: true, operatingPeriod: { - fromDate: today, - toDate: tomorrow, + fromDate: today.toString(), + toDate: tomorrow.toString(), }, }; onChange([...dayTypeAssignments, dayTypeAssignment]); @@ -49,7 +49,7 @@ const DayTypeAssignmentsEditor = ({ dayTypeAssignments, onChange }: Props) => { }; const isNotBefore = (toDate: string, fromDate: string): boolean => - !moment(toDate).isBefore(moment(fromDate)); + parseAbsoluteToLocal(toDate).compare(parseAbsoluteToLocal(fromDate)) > -1; if (dayTypeAssignments.length === 0) addNewDayTypeAssignment(); @@ -74,7 +74,7 @@ const DayTypeAssignmentsEditor = ({ dayTypeAssignments, onChange }: Props) => { { changeDay( @@ -102,7 +102,7 @@ const DayTypeAssignmentsEditor = ({ dayTypeAssignments, onChange }: Props) => { false, )} selectedDate={nativeDateToDateValue( - moment(dta.operatingPeriod.toDate).toDate(), + new Date(dta.operatingPeriod.toDate), )} onChange={(date) => { changeDay( diff --git a/src/components/DurationPicker/index.tsx b/src/components/DurationPicker/index.tsx index 4171e3f7f..c77f8bc2b 100644 --- a/src/components/DurationPicker/index.tsx +++ b/src/components/DurationPicker/index.tsx @@ -1,8 +1,5 @@ import cx from 'classnames'; -import formatDuration from 'date-fns/formatDuration'; -import { nb } from 'date-fns/locale'; import * as durationLib from 'duration-fns'; -import moment from 'moment'; import { useIntl } from 'react-intl'; import TimeUnitPicker, { TimeUnitPickerPosition } from '../TimeUnitPicker'; @@ -21,6 +18,59 @@ type Props = { disabled?: boolean; }; +const formatDuration = (duration: any, intl: any) => { + const parts = []; + if (duration.years) { + parts.push( + intl.formatMessage( + { id: 'duration.years', defaultMessage: '{years} years' }, + { years: duration.years }, + ), + ); + } + if (duration.months) { + parts.push( + intl.formatMessage( + { id: 'duration.months', defaultMessage: '{months} months' }, + { months: duration.months }, + ), + ); + } + if (duration.days) { + parts.push( + intl.formatMessage( + { id: 'duration.days', defaultMessage: '{days} days' }, + { days: duration.days }, + ), + ); + } + if (duration.hours) { + parts.push( + intl.formatMessage( + { id: 'duration.hours', defaultMessage: '{hours} hours' }, + { hours: duration.hours }, + ), + ); + } + if (duration.minutes) { + parts.push( + intl.formatMessage( + { id: 'duration.minutes', defaultMessage: '{minutes} minutes' }, + { minutes: duration.minutes }, + ), + ); + } + if (duration.seconds) { + parts.push( + intl.formatMessage( + { id: 'duration.seconds', defaultMessage: '{seconds} seconds' }, + { seconds: duration.seconds }, + ), + ); + } + return parts.join(', '); +}; + export default (props: Props) => { const { onChange, @@ -44,10 +94,7 @@ export default (props: Props) => { const durationObj = durationLib.parse(duration); return { ...durationObj, - textValue: formatDuration(durationObj, { - locale: intl.locale === 'nb' ? nb : undefined, - delimiter: ', ', - }), + textValue: formatDuration(durationObj, intl), }; } else { return undefined; @@ -55,19 +102,19 @@ export default (props: Props) => { })(); const handleOnUnitChange = (unit: string, value: number) => { - const newDuration = moment.duration({ + const newDuration = { seconds: unit === 'seconds' ? value : parsedDuration?.seconds, minutes: unit === 'minutes' ? value : parsedDuration?.minutes, hours: unit === 'hours' ? value : parsedDuration?.hours, days: unit === 'days' ? value : parsedDuration?.days, months: unit === 'months' ? value : parsedDuration?.months, years: unit === 'years' ? value : parsedDuration?.years, - }); + }; onChange( - resetOnZero && newDuration.asSeconds() === 0 + resetOnZero && Object.values(newDuration).every((val) => val === 0) ? undefined - : newDuration.toISOString(), + : JSON.stringify(newDuration), ); }; diff --git a/src/components/LinesForExport/LinesForExport.test.tsx b/src/components/LinesForExport/LinesForExport.test.tsx index c77ccfc11..5d343920b 100644 --- a/src/components/LinesForExport/LinesForExport.test.tsx +++ b/src/components/LinesForExport/LinesForExport.test.tsx @@ -1,5 +1,5 @@ import { MockedProvider } from '@apollo/client/testing'; -import { addDays, format, subDays } from 'date-fns'; +import { getCurrentDate } from '../../utils/dates'; import { MemoryRouter } from 'react-router-dom'; import { GET_LINES_FOR_EXPORT } from 'api/uttu/queries'; @@ -30,8 +30,8 @@ const line = { dayTypeAssignments: [ { operatingPeriod: { - fromDate: format(addDays(new Date(), 10), 'yyyy-MM-dd'), - toDate: format(addDays(new Date(), 130), 'yyyy-MM-dd'), + fromDate: getCurrentDate().add({ days: 10 }).toString(), + toDate: getCurrentDate().add({ days: 130 }).toString(), }, }, ], @@ -55,8 +55,10 @@ const secondLine = { dayTypeAssignments: [ { operatingPeriod: { - fromDate: format(subDays(new Date(), 30), 'yyyy-MM-dd'), - toDate: format(subDays(new Date(), 10), 'yyyy-MM-dd'), + fromDate: getCurrentDate() + .subtract({ days: 30 }) + .toString(), + toDate: getCurrentDate().subtract({ days: 10 }).toString(), }, }, ], @@ -80,8 +82,8 @@ const flexibleLine = { dayTypeAssignments: [ { operatingPeriod: { - fromDate: format(new Date(), 'yyyy-MM-dd'), - toDate: format(addDays(new Date(), 10), 'yyyy-MM-dd'), + fromDate: getCurrentDate().toString(), + toDate: getCurrentDate().add({ days: 10 }).toString(), }, }, ], diff --git a/src/components/LinesForExport/index.tsx b/src/components/LinesForExport/index.tsx index 8de176ddc..21e8ce2ca 100644 --- a/src/components/LinesForExport/index.tsx +++ b/src/components/LinesForExport/index.tsx @@ -11,8 +11,6 @@ import { } from '@entur/table'; import { SmallText, StrongText } from '@entur/typography'; import { GET_LINES_FOR_EXPORT } from 'api/uttu/queries'; -import { differenceInCalendarDays, isAfter, isBefore } from 'date-fns'; -import parseDate from 'date-fns/parseISO'; import useRefetchOnLocationChange from 'hooks/useRefetchOnLocationChange'; import { ExportLineAssociation } from 'model/Export'; import FlexibleLine from 'model/FlexibleLine'; @@ -21,6 +19,13 @@ import Line from 'model/Line'; import OperatingPeriod from 'model/OperatingPeriod'; import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; +import { + parseISOToCalendarDate, + getCurrentDate, + CalendarDate, +} from '../../utils/dates'; +import { getLocale } from 'i18n/getLocale'; +import { DateFormatter, getLocalTimeZone } from '@internationalized/date'; type Props = { onChange: (lines: ExportLineAssociation[]) => void; @@ -37,20 +42,20 @@ type ExportableLine = { id: string; name: string; status: Status; - from: Date; - to: Date; + from: CalendarDate; + to: CalendarDate; selected: boolean; }; type Availability = { - from: Date; - to: Date; + from: CalendarDate; + to: CalendarDate; }; const union = (left: Availability, right: Availability): Availability => { return { - from: isBefore(right.from, left.from) ? right.from : left.from, - to: isAfter(right.to, left.to) ? right.to : left.to, + from: right.from.compare(left.from) < 0 ? right.from : left.from, + to: right.to.compare(left.to) > 0 ? right.to : left.to, }; }; @@ -59,8 +64,8 @@ const mergeAvailability = ( operatingPeriod: OperatingPeriod, ) => { const availabilityFromOperatingPeriod = { - from: parseDate(operatingPeriod.fromDate), - to: parseDate(operatingPeriod.toDate), + from: parseISOToCalendarDate(operatingPeriod.fromDate)!, + to: parseISOToCalendarDate(operatingPeriod.toDate)!, }; return availability @@ -93,12 +98,9 @@ const getAvailability = (journeyPatterns?: JourneyPattern[]): Availability => { }; const mapLine = ({ id, name, journeyPatterns }: Line): ExportableLine => { - const today = new Date(); + const today = getCurrentDate(); const jpAvailability = getAvailability(journeyPatterns); - const availableForDaysFromNow = differenceInCalendarDays( - jpAvailability.to, - today, - ); + const availableForDaysFromNow = jpAvailability.to.compare(today); let status: Status; @@ -254,7 +256,7 @@ export default ({ onChange }: Props) => { {mapStatusToText(line.status)} - {`${line.from.toLocaleDateString()} - ${line.to.toLocaleDateString()}`} + {`${new DateFormatter(getLocale().toString()).format(line.from.toDate(getLocalTimeZone()))} - ${new DateFormatter(getLocale().toString()).format(line.to.toDate(getLocalTimeZone()))}`} ))} diff --git a/src/components/ServiceJourneyEditor/CopyDialog.test.tsx b/src/components/ServiceJourneyEditor/CopyDialog.test.tsx index 11df7cde2..780c44aba 100644 --- a/src/components/ServiceJourneyEditor/CopyDialog.test.tsx +++ b/src/components/ServiceJourneyEditor/CopyDialog.test.tsx @@ -41,15 +41,15 @@ describe('copyServiceJourney', () => { duration.parse('PT10M'), ); - expect(copies.length).toBe(7); + expect(copies.length).toBe(6); - expect(copies[0].passingTimes[0].arrivalTime).toBe('12:00:00'); - expect(copies[0].passingTimes[0].departureTime).toBe('12:00:00'); - expect(copies[0].passingTimes[1].arrivalTime).toBe('12:03:00'); - expect(copies[0].passingTimes[1].departureTime).toBe('12:03:00'); - expect(copies[0].passingTimes[2].arrivalTime).toBe('12:05:00'); - expect(copies[0].passingTimes[2].departureTime).toBe('12:05:00'); - expect(copies[0].passingTimes[3].arrivalTime).toBe('12:10:00'); - expect(copies[0].passingTimes[3].departureTime).toBe('12:10:00'); + expect(copies[5].passingTimes[0].arrivalTime).toBe('12:00:00'); + expect(copies[5].passingTimes[0].departureTime).toBe('12:00:00'); + expect(copies[5].passingTimes[1].arrivalTime).toBe('12:03:00'); + expect(copies[5].passingTimes[1].departureTime).toBe('12:03:00'); + expect(copies[5].passingTimes[2].arrivalTime).toBe('12:05:00'); + expect(copies[5].passingTimes[2].departureTime).toBe('12:05:00'); + expect(copies[5].passingTimes[3].arrivalTime).toBe('12:10:00'); + expect(copies[5].passingTimes[3].departureTime).toBe('12:10:00'); }); }); diff --git a/src/components/ServiceJourneyEditor/CopyDialog.tsx b/src/components/ServiceJourneyEditor/CopyDialog.tsx index bb81ac244..c1b029f8c 100644 --- a/src/components/ServiceJourneyEditor/CopyDialog.tsx +++ b/src/components/ServiceJourneyEditor/CopyDialog.tsx @@ -7,15 +7,18 @@ import { import { FeedbackText, Switch, TextField } from '@entur/form'; import { Modal } from '@entur/modal'; import { Label } from '@entur/typography'; +import { + CalendarDateTime, + fromDate, + getLocalTimeZone, + parseTime, + Time, + toCalendarDate, + ZonedDateTime, +} from '@internationalized/date'; import { TimeValue } from '@react-types/datepicker'; import DayOffsetDropdown from 'components/DayOffsetDropdown'; import DurationPicker from 'components/DurationPicker'; -import { - addDays, - addMinutes, - differenceInCalendarDays, - differenceInMinutes, -} from 'date-fns'; import * as duration from 'duration-fns'; import { createUuid } from 'helpers/generators'; import { isAfter, isBefore } from 'helpers/validation'; @@ -41,7 +44,7 @@ const toDate = (date: string): Date => { const dateObj = new Date(); dateObj.setHours(parseInt(hours)); dateObj.setMinutes(parseInt(minutes)); - dateObj.setSeconds(parseInt(seconds)); + dateObj.setSeconds(parseInt(seconds || '0')); dateObj.setMilliseconds(0); return dateObj; }; @@ -54,11 +57,16 @@ const offsetPassingTime = ( dayOffsetKey: string, ) => { if (!oldTime) return undefined; - const newTime = addMinutes(toDate(oldTime), offset); - const offsetDays = differenceInCalendarDays(newTime, toDate(oldTime)); + const oldTimeAsDate = fromDate(toDate(oldTime), getLocalTimeZone()); + const newTimeAsDate = fromDate(toDate(oldTime), getLocalTimeZone()).add({ + minutes: offset, + }); + const offsetDays = + newTimeAsDate.calendar.toJulianDay(newTimeAsDate) - + oldTimeAsDate.calendar.toJulianDay(oldTimeAsDate); return { - [timeKey]: newTime.toTimeString().split(' ')[0], + [timeKey]: newTimeAsDate.toDate().toTimeString().split(' ')[0], [dayOffsetKey]: oldDayOffset + offsetDays, }; }; @@ -104,24 +112,19 @@ export const copyServiceJourney = ( dayOffset: number, untilTime: string, untilDayOffset: number, - repeatDuration: Duration, + repeatDuration: duration.Duration, ): ServiceJourney[] => { - const departure = addDays(toDate(departureTime), dayOffset); - if (isAfter(departureTime, dayOffset, untilTime, untilDayOffset)) { return newServiceJourneys; } else { - const lastActualDeparture = addDays( - toDate(serviceJourney.passingTimes[0].departureTime!), - serviceJourney.passingTimes[0].departureDayOffset!, - ); - const { id, passingTimes, dayTypesRefs, ...copyableServiceJourney } = serviceJourney; + const departure = parseTime(departureTime); + const newPassingTimes = offsetPassingTimes( passingTimes.map(({ id, ...pt }) => pt), - differenceInMinutes(departure, lastActualDeparture), + repeatDuration.minutes, ); const newServiceJourney = { @@ -135,20 +138,18 @@ export const copyServiceJourney = ( passingTimes: newPassingTimes, }; newServiceJourneys.push(newServiceJourney); - const nextDeparture = addMinutes( - departure, - duration.toMinutes(repeatDuration), - ); - const nextDepartureTime = nextDeparture?.toTimeString().split(' ')[0]; - const nextDayOffset = differenceInCalendarDays( - nextDeparture, - toDate(departureTime), - ); + + const nextDeparture = departure.add({ + minutes: duration.toMinutes(repeatDuration), + }); + const nextDayOffset = + departure.hour < nextDeparture.hour ? dayOffset + 1 : dayOffset; + return copyServiceJourney( newServiceJourney, newServiceJourneys, nameTemplate, - nextDepartureTime, + nextDeparture.toString(), nextDayOffset, untilTime, untilDayOffset, diff --git a/src/custom.typings.d.ts b/src/custom.typings.d.ts index 00ea8e50a..80aac421f 100644 --- a/src/custom.typings.d.ts +++ b/src/custom.typings.d.ts @@ -1,4 +1,4 @@ -import 'date-fns/formatDuration'; +// No need for date-fns type import anymore // Awaiting release of https://github.com/date-fns/date-fns/pull/1881 declare module 'date-fns/formatDuration' { diff --git a/src/helpers/dates.ts b/src/helpers/dates.ts index 3405263fb..606491343 100644 --- a/src/helpers/dates.ts +++ b/src/helpers/dates.ts @@ -1,4 +1,11 @@ -import moment from 'moment'; +import { + fromDate, + getLocalTimeZone, + toCalendarDate, +} from '@internationalized/date'; + +const dateString = (date: Date): string => + `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; export const dateToString = (date: Date | null): string => - moment(date ?? undefined).format('YYYY-MM-DD'); + date ? dateString(date) : dateString(new Date()); diff --git a/src/helpers/validation.ts b/src/helpers/validation.ts index f45084985..ed90d3bab 100644 --- a/src/helpers/validation.ts +++ b/src/helpers/validation.ts @@ -1,10 +1,3 @@ -import { - addDays, - getDay, - isAfter as isDateAfter, - isEqual as isDateEqual, - parseISO, -} from 'date-fns'; import { isBlank, objectValuesAreEmpty } from 'helpers/forms'; import { MessagesKey } from 'i18n/translationKeys'; import BookingArrangement from 'model/BookingArrangement'; @@ -15,8 +8,22 @@ import Line from 'model/Line'; import PassingTime from 'model/PassingTime'; import ServiceJourney from 'model/ServiceJourney'; import StopPoint from 'model/StopPoint'; -import moment from 'moment'; import { IntlShape } from 'react-intl'; +import { + parseTime, + Time, + CalendarDateTime, + parseDate, +} from '@internationalized/date'; +import { getCurrentDateTime } from 'utils/dates'; + +const addDays = (time: Time, days: number): CalendarDateTime => + getCurrentDateTime() + .set({ + hour: time.hour, + minute: time.minute, + }) + .add({ days }); export const validLine = (line: Line, intl: IntlShape): boolean => aboutLineStepIsValid(line) && @@ -255,13 +262,16 @@ export const isBefore = ( nextDayOffset: number | undefined, ) => { if (!passingTime || !nextPassingTime) return false; - const date = moment(passingTime, 'HH:mm:ss').add(dayOffset ?? 0, 'days'); - const nextDate = moment(nextPassingTime, 'HH:mm:ss').add( - nextDayOffset ?? 0, - 'days', - ); - return date < nextDate; + try { + const time = parseTime(passingTime); + const nextTime = parseTime(nextPassingTime); + const date = addDays(time, dayOffset ?? 0); + const nextDate = addDays(nextTime, nextDayOffset ?? 0); + return date.compare(nextDate) < 0; + } catch (e) { + return false; + } }; export const isAfter = ( @@ -271,13 +281,16 @@ export const isAfter = ( nextDayOffset: number | undefined, ) => { if (!passingTime || !nextPassingTime) return false; - const date = moment(passingTime, 'HH:mm:ss').add(dayOffset ?? 0, 'days'); - const nextDate = moment(nextPassingTime, 'HH:mm:ss').add( - nextDayOffset ?? 0, - 'days', - ); - return date > nextDate; + try { + const time = parseTime(passingTime); + const nextTime = parseTime(nextPassingTime); + const date = addDays(time, dayOffset ?? 0); + const nextDate = addDays(nextTime, nextDayOffset ?? 0); + return date.compare(nextDate) > 0; + } catch (e) { + return false; + } }; const hasAtleastOneFieldSet = (passingTime: PassingTime) => { @@ -472,14 +485,14 @@ export const validateDayType = (dayType: DayType) => { dayType.daysOfWeek?.map((dow) => WEEKDAYS.indexOf(dow)) || []; return dayType.dayTypeAssignments.every((dta) => { - let from = parseISO(dta.operatingPeriod.fromDate); - const to = parseISO(dta.operatingPeriod.toDate); + let from = parseDate(dta.operatingPeriod.fromDate); + const to = parseDate(dta.operatingPeriod.toDate); - while (isDateEqual(to, from) || isDateAfter(to, from)) { - if (daysOfWeek.includes(getDay(from))) { + while (to.compare(from) < 1) { + if (daysOfWeek.includes(from.day)) { return true; } - from = addDays(from, 1); + from = from.add({ days: 1 }); } return false; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index c0890df96..6af97d9f7 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,4 +1,3 @@ -import 'moment/locale/nb'; export { getMessages } from './getMessages'; export type { FormatMessage } from './getMessages'; export { diff --git a/src/model/DayTypeAssignment.ts b/src/model/DayTypeAssignment.ts index 48103faf4..9fec4224e 100644 --- a/src/model/DayTypeAssignment.ts +++ b/src/model/DayTypeAssignment.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import { getCurrentDate } from '../utils/dates'; import OperatingPeriod from './OperatingPeriod'; import VersionedType from './VersionedType'; @@ -11,8 +11,8 @@ export type DayTypeAssignment = VersionedType & { export const newDayTypeAssignment = (): DayTypeAssignment => ({ isAvailable: true, operatingPeriod: { - fromDate: moment().format('YYYY-MM-DD'), - toDate: moment().format('YYYY-MM-DD'), + fromDate: getCurrentDate().toString(), + toDate: getCurrentDate().toString(), }, }); diff --git a/src/utils/dates.ts b/src/utils/dates.ts new file mode 100644 index 000000000..482bbaa42 --- /dev/null +++ b/src/utils/dates.ts @@ -0,0 +1,59 @@ +import { + CalendarDate, + CalendarDateTime, + getLocalTimeZone, + now, + parseDate, + parseDateTime, + toCalendarDate, + toCalendarDateTime, + today, +} from '@internationalized/date'; + +export function parseISOToCalendarDate( + isoString: string | null | undefined, +): CalendarDate | null { + if (!isoString) return null; + try { + return parseDate(isoString); + } catch (e) { + console.error('Failed to parse ISO string to CalendarDate:', e); + return null; + } +} + +export function parseISOToCalendarDateTime( + isoString: string | null | undefined, +): CalendarDateTime | null { + if (!isoString) return null; + try { + return parseDateTime(isoString); + } catch (e) { + console.error('Failed to parse ISO string to CalendarDateTime:', e); + return null; + } +} + +export function calendarDateToISO( + date: CalendarDate | null | undefined, +): string | null { + if (!date) return null; + return date.toString(); +} + +export function calendarDateTimeToISO( + dateTime: CalendarDateTime | null | undefined, +): string | null { + if (!dateTime) return null; + return dateTime.toString(); +} + +export function getCurrentDate(): CalendarDate { + return toCalendarDate(today(getLocalTimeZone())); +} + +export function getCurrentDateTime(): CalendarDateTime { + return toCalendarDateTime(now(getLocalTimeZone())); +} + +export type { CalendarDate, CalendarDateTime } from '@internationalized/date'; From 61d2351f8376da2203266a7d38b1c4f146356040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Erik=20St=C3=B8wer?= Date: Mon, 2 Dec 2024 15:51:17 +0100 Subject: [PATCH 2/2] fix bug --- .../ServiceJourneyEditor/CopyDialog.tsx | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/src/components/ServiceJourneyEditor/CopyDialog.tsx b/src/components/ServiceJourneyEditor/CopyDialog.tsx index c1b029f8c..2ef481948 100644 --- a/src/components/ServiceJourneyEditor/CopyDialog.tsx +++ b/src/components/ServiceJourneyEditor/CopyDialog.tsx @@ -11,6 +11,7 @@ import { CalendarDateTime, fromDate, getLocalTimeZone, + now, parseTime, Time, toCalendarDate, @@ -104,6 +105,10 @@ const offsetPassingTimes = ( ); }; +const addDays = (time: Time, days: number) => { + return now(getLocalTimeZone()).set(time).add({ days }); +}; + export const copyServiceJourney = ( serviceJourney: ServiceJourney, newServiceJourneys: ServiceJourney[], @@ -114,48 +119,54 @@ export const copyServiceJourney = ( untilDayOffset: number, repeatDuration: duration.Duration, ): ServiceJourney[] => { - if (isAfter(departureTime, dayOffset, untilTime, untilDayOffset)) { + const departure = parseTime(departureTime); + const until = parseTime(untilTime); + + // Compare times including day offset + const departureDateTime = addDays(departure, dayOffset); + const untilDateTime = addDays(until, untilDayOffset); + + if (departureDateTime.compare(untilDateTime) >= 0) { return newServiceJourneys; - } else { - const { id, passingTimes, dayTypesRefs, ...copyableServiceJourney } = - serviceJourney; + } - const departure = parseTime(departureTime); + const { id, passingTimes, dayTypesRefs, ...copyableServiceJourney } = + serviceJourney; - const newPassingTimes = offsetPassingTimes( - passingTimes.map(({ id, ...pt }) => pt), - repeatDuration.minutes, - ); + const minutesOffset = duration.toMinutes(repeatDuration); - const newServiceJourney = { - ...cloneDeep(copyableServiceJourney), - id: `new_${createUuid()}`, - name: nameTemplate.replace( - '<% time %>', - `${departureTime.slice(0, -3)} +${dayOffset}`, - ), - dayTypesRefs, - passingTimes: newPassingTimes, - }; - newServiceJourneys.push(newServiceJourney); + const newPassingTimes = offsetPassingTimes( + passingTimes.map(({ id, ...pt }) => pt), + minutesOffset, + ); - const nextDeparture = departure.add({ - minutes: duration.toMinutes(repeatDuration), - }); - const nextDayOffset = - departure.hour < nextDeparture.hour ? dayOffset + 1 : dayOffset; + const newServiceJourney = { + ...cloneDeep(copyableServiceJourney), + id: `new_${createUuid()}`, + name: nameTemplate.replace( + '<% number %>', + (newServiceJourneys.length + 1).toString(), + ), + dayTypesRefs, + passingTimes: newPassingTimes, + }; + newServiceJourneys.push(newServiceJourney); - return copyServiceJourney( - newServiceJourney, - newServiceJourneys, - nameTemplate, - nextDeparture.toString(), - nextDayOffset, - untilTime, - untilDayOffset, - repeatDuration, - ); - } + // Calculate next departure time + const nextDeparture = departure.add({ minutes: minutesOffset }); + const nextDayOffset = + dayOffset + (nextDeparture.hour < departure.hour ? 1 : 0); + + return copyServiceJourney( + newServiceJourney, + newServiceJourneys, + nameTemplate, + nextDeparture.toString(), + nextDayOffset, + untilTime, + untilDayOffset, + repeatDuration, + ); }; export default ({ open, serviceJourney, onSave, onDismiss }: Props) => { @@ -165,7 +176,7 @@ export default ({ open, serviceJourney, onSave, onDismiss }: Props) => { serviceJourney.passingTimes[0].departureDayOffset || 0; const [nameTemplate, setNameTemplate] = useState( - `${serviceJourney.name || 'New'} (<% time %>)`, + `${serviceJourney.name || 'New'} (<% number %>)`, ); const [initialDepartureTime, setInitialDepartureTime] = useState(defaultDepartureTime);