From f064e41cb4e880eb9a38c80f39b1324eb451eb6c Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 10 Feb 2025 17:27:25 +0000 Subject: [PATCH] fix: use correct start dates on omnichannel reports (#35100) --- .changeset/kind-geckos-heal.md | 5 + .../dashboards/getClosedPeriod.spec.ts | 101 ++++++++++++++++++ .../client/components/dashboards/periods.ts | 62 ++++++----- .../reports/utils/formatPeriodRange.ts | 2 +- 4 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 .changeset/kind-geckos-heal.md create mode 100644 apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts diff --git a/.changeset/kind-geckos-heal.md b/.changeset/kind-geckos-heal.md new file mode 100644 index 0000000000000..c516facb447d6 --- /dev/null +++ b/.changeset/kind-geckos-heal.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes incorrect start date on omnichannel reports diff --git a/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts new file mode 100644 index 0000000000000..518f5843fa0d4 --- /dev/null +++ b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts @@ -0,0 +1,101 @@ +import { getClosedPeriod } from './periods'; + +jest.mock('moment', () => { + return () => jest.requireActual('moment')('2024-05-19T12:00:00.000Z'); +}); + +it('should return the correct period range for this month', () => { + const monthExpectedReturn = { + start: new Date('5/1/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'month' })(true); + + expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); +}); + +it('should return the correct period range for this year', () => { + const yearExpectedReturn = { + start: new Date('1/1/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'year' })(true); + + expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); +}); + +it('should return the correct period range for last 6 months', () => { + const last6MonthsExpectedReturn = { + start: new Date('11/1/2023').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); + + expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); +}); + +it('should return the correct period range for this week', () => { + const weekExpectedReturn = { + start: new Date('5/19/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'week' })(true); + + expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); +}); + +it('should return the correct period range for this month using local time', () => { + const monthExpectedReturn = { + start: new Date('5/1/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'month' })(false); + + expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); +}); + +it('should return the correct period range for this year using local time', () => { + const yearExpectedReturn = { + start: new Date('1/1/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'year' })(false); + + expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); +}); + +it('should return the correct period range for last 6 months using local time', () => { + const last6MonthsExpectedReturn = { + start: new Date('11/1/2023').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); + + expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); +}); + +it('should return the correct period range for this week using local time', () => { + const weekExpectedReturn = { + start: new Date('5/19/2024').toISOString().split('T')[0], + end: new Date('5/19/2024').toISOString().split('T')[0], + }; + + const period = getClosedPeriod({ startOf: 'week' })(false); + + expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); + expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); +}); diff --git a/apps/meteor/client/components/dashboards/periods.ts b/apps/meteor/client/components/dashboards/periods.ts index 824064b99d6c0..5ae1626e976cf 100644 --- a/apps/meteor/client/components/dashboards/periods.ts +++ b/apps/meteor/client/components/dashboards/periods.ts @@ -1,78 +1,90 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { DurationInputArg1, DurationInputArg2 } from 'moment'; import moment from 'moment'; const label = (translationKey: TranslationKey): readonly [translationKey: TranslationKey] => [translationKey]; -const lastNDays = - ( - n: number, - ): ((utc: boolean) => { +export const getClosedPeriod = + ({ + startOf, + subtract, + }: { + startOf: 'day' | 'year' | 'month' | 'week'; + subtract?: { amount: DurationInputArg1; unit: DurationInputArg2 }; + }): ((utc: boolean) => { start: Date; end: Date; }) => (utc): { start: Date; end: Date } => { const date = new Date(); const offsetForMoment = -(date.getTimezoneOffset() / 60); + let start = moment(date).utc(); + let end = moment(date).utc(); - const start = utc - ? moment.utc().startOf('day').subtract(n, 'days').toDate() - : moment().subtract(n, 'days').startOf('day').utcOffset(offsetForMoment).toDate(); + if (subtract) { + const { amount, unit } = subtract; + start.subtract(amount, unit); + } - const end = utc ? moment.utc().endOf('day').toDate() : moment().endOf('day').utcOffset(offsetForMoment).toDate(); + if (!utc) { + start = start.utcOffset(offsetForMoment); + end = end.utcOffset(offsetForMoment); + } - return { start, end }; - }; + // moment.toDate() can only return the date in localtime, that's why we do the new Date conversion + // https://github.com/moment/moment-timezone/issues/644 -const getLast6Months = () => { - const last6Months = moment().subtract(6, 'months'); - return moment().diff(last6Months, 'days'); -}; + return { + start: new Date(start.startOf(startOf).format('YYYY-MM-DD HH:mm:ss')), + end: new Date(end.endOf('day').format('YYYY-MM-DD HH:mm:ss')), + }; + }; const periods = [ { key: 'today', label: label('Today'), - range: lastNDays(0), + range: getClosedPeriod({ startOf: 'day' }), }, { key: 'this week', label: label('This_week'), - range: lastNDays(moment().day()), + range: getClosedPeriod({ startOf: 'week' }), }, { key: 'last 7 days', label: label('Last_7_days'), - range: lastNDays(7), + range: getClosedPeriod({ startOf: 'day', subtract: { amount: 7, unit: 'days' } }), }, { key: 'last 15 days', label: label('Last_15_days'), - range: lastNDays(15), + range: getClosedPeriod({ startOf: 'day', subtract: { amount: 15, unit: 'days' } }), }, { key: 'this month', label: label('This_month'), - range: lastNDays(moment().date()), + range: getClosedPeriod({ startOf: 'month' }), }, { key: 'last 30 days', label: label('Last_30_days'), - range: lastNDays(30), + range: getClosedPeriod({ startOf: 'day', subtract: { amount: 30, unit: 'days' } }), }, { key: 'last 90 days', label: label('Last_90_days'), - range: lastNDays(90), + range: getClosedPeriod({ startOf: 'day', subtract: { amount: 90, unit: 'days' } }), }, { key: 'last 6 months', label: label('Last_6_months'), - range: lastNDays(getLast6Months()), + range: getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } }), }, { key: 'this year', label: label('This_year'), - range: lastNDays(moment().dayOfYear()), + range: getClosedPeriod({ startOf: 'year' }), }, ] as const; @@ -82,7 +94,7 @@ export const getPeriod = (key: (typeof periods)[number]['key']): Period => { const period = periods.find((period) => period.key === key); if (!period) { - throw new Error(`"${key}" is not a valid period key`); + return periods[0]; } return period; @@ -98,7 +110,7 @@ export const getPeriodRange = ( const period = periods.find((period) => period.key === key); if (!period) { - throw new Error(`"${key}" is not a valid period key`); + return periods[0].range(utc); } return period.range(utc); diff --git a/apps/meteor/client/omnichannel/reports/utils/formatPeriodRange.ts b/apps/meteor/client/omnichannel/reports/utils/formatPeriodRange.ts index a6272f3795c54..ab79c60514c0c 100644 --- a/apps/meteor/client/omnichannel/reports/utils/formatPeriodRange.ts +++ b/apps/meteor/client/omnichannel/reports/utils/formatPeriodRange.ts @@ -2,7 +2,7 @@ import type { Period } from '../../../components/dashboards/periods'; import { getPeriodRange } from '../../../components/dashboards/periods'; const formatDate = (date: Date): string => { - const day = String(date.getUTCDate()).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear();