diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts index 3072341639..6b89613093 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts @@ -9,5 +9,6 @@ export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnitOption[] = [ { value: 'DAY', label: 'Day' }, { value: 'WEEK', label: 'Week' }, { value: 'MONTH', label: 'Month' }, + { value: 'QUARTER', label: 'Quarter' }, { value: 'YEAR', label: 'Year' }, ]; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts index a3d8ff5044..7e4dc916a7 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts @@ -1,7 +1,7 @@ import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; +import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter'; import { FirstDayOfTheWeek } from 'twenty-shared/types'; import { type RelativeDateFilter } from 'twenty-shared/utils'; -import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter'; jest.mock('@/localization/utils/detection/detectCalendarStartDay'); @@ -68,6 +68,11 @@ describe('stringifyRelativeDateFilter', () => { amount: 1, unit: 'MONTH', }; + const quarterFilter: RelativeDateFilter = { + direction: 'PAST', + amount: 1, + unit: 'QUARTER', + }; const yearFilter: RelativeDateFilter = { direction: 'PAST', amount: 1, @@ -80,6 +85,7 @@ describe('stringifyRelativeDateFilter', () => { expect(stringifyRelativeDateFilter(dayFilter)).toBe('PAST_1_DAY'); expect(stringifyRelativeDateFilter(weekFilter)).toBe('PAST_1_WEEK'); expect(stringifyRelativeDateFilter(monthFilter)).toBe('PAST_1_MONTH'); + expect(stringifyRelativeDateFilter(quarterFilter)).toBe('PAST_1_QUARTER'); expect(stringifyRelativeDateFilter(yearFilter)).toBe('PAST_1_YEAR'); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts index 9a530f70d4..69f605ad4e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts @@ -4,6 +4,7 @@ import { endOfHour, endOfMinute, endOfMonth, + endOfQuarter, endOfSecond, endOfWeek, endOfYear, @@ -12,12 +13,14 @@ import { startOfHour, startOfMinute, startOfMonth, + startOfQuarter, startOfSecond, startOfWeek, startOfYear, } from 'date-fns'; import { addUnitToDateTime, + assertUnreachable, getFirstDayOfTheWeekAsANumberForDateFNS, isDefined, type RelativeDateFilter, @@ -169,7 +172,12 @@ function evaluateThisDirection( start: startOfYear(now), end: endOfYear(now), }); + case 'QUARTER': + return isWithinInterval(dateToCheck, { + start: startOfQuarter(now), + end: endOfQuarter(now), + }); default: - return false; + return assertUnreachable(unit); } } diff --git a/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts b/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts index 7fc5695523..73dc37cfd5 100644 --- a/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts +++ b/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts @@ -1,3 +1,3 @@ import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema'; -export type DateTimePeriod = RelativeDateFilterUnit | 'QUARTER'; +export type DateTimePeriod = RelativeDateFilterUnit; diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts index 49cec63a0d..d9457b2412 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts @@ -62,6 +62,44 @@ describe('relativeDateFilterStringifiedSchema', () => { } }); + it('should parse a valid PAST QUARTER filter string', () => { + const result = + relativeDateFilterStringifiedSchema.safeParse('PAST_2_QUARTER'); + + expect(result.success).toBe(true); + + if (result.success) { + expect(result.data.direction).toBe('PAST'); + expect(result.data.amount).toBe(2); + expect(result.data.unit).toBe('QUARTER'); + } + }); + + it('should parse a THIS QUARTER filter string', () => { + const result = + relativeDateFilterStringifiedSchema.safeParse('THIS_1_QUARTER'); + + expect(result.success).toBe(true); + + if (result.success) { + expect(result.data.direction).toBe('THIS'); + expect(result.data.unit).toBe('QUARTER'); + } + }); + + it('should parse a valid NEXT QUARTER filter string', () => { + const result = + relativeDateFilterStringifiedSchema.safeParse('NEXT_1_QUARTER'); + + expect(result.success).toBe(true); + + if (result.success) { + expect(result.data.direction).toBe('NEXT'); + expect(result.data.amount).toBe(1); + expect(result.data.unit).toBe('QUARTER'); + } + }); + it('should fail for invalid strings', () => { const result = relativeDateFilterStringifiedSchema.safeParse('INVALID_STRING'); diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts index a08c0697e6..84fc9aa46e 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts @@ -67,5 +67,35 @@ describe('resolveRelativeDateFilter', () => { expect(result.start).toBe('2024-01-01'); expect(result.end).toBe('2025-01-01'); }); + + it('should compute start and end for THIS QUARTER', () => { + const result = resolveRelativeDateFilter( + { direction: 'THIS', amount: 1, unit: 'QUARTER' }, + referenceZdt, + ); + + expect(result.start).toBe('2024-01-01'); + expect(result.end).toBe('2024-04-01'); + }); + + it('should compute start and end for PAST 1 QUARTER', () => { + const result = resolveRelativeDateFilter( + { direction: 'PAST', amount: 1, unit: 'QUARTER' }, + referenceZdt, + ); + + expect(result.start).toBe('2023-10-01'); + expect(result.end).toBe('2024-01-01'); + }); + + it('should compute start and end for NEXT 1 QUARTER', () => { + const result = resolveRelativeDateFilter( + { direction: 'NEXT', amount: 1, unit: 'QUARTER' }, + referenceZdt, + ); + + expect(result.start).toBe('2024-04-01'); + expect(result.end).toBe('2024-07-01'); + }); }); }); diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts index 181040cae3..029f22ee4a 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts @@ -26,6 +26,18 @@ describe('resolveRelativeDateTimeFilter', () => { expect(result.end?.day).toBe(23); }); + it('should compute for NEXT 1 QUARTER', () => { + const result = resolveRelativeDateTimeFilter( + { direction: 'NEXT', amount: 1, unit: 'QUARTER' }, + referenceZdt, + ); + + expect(result.start?.month).toBe(4); + expect(result.start?.day).toBe(1); + expect(result.end?.month).toBe(7); + expect(result.end?.day).toBe(1); + }); + it('should throw if amount is undefined', () => { expect(() => resolveRelativeDateTimeFilter( @@ -58,6 +70,20 @@ describe('resolveRelativeDateTimeFilter', () => { expect(result.start?.day).toBe(12); }); + it('should compute for PAST 1 QUARTER', () => { + const result = resolveRelativeDateTimeFilter( + { direction: 'PAST', amount: 1, unit: 'QUARTER' }, + referenceZdt, + ); + + expect(result.start?.month).toBe(10); + expect(result.start?.year).toBe(2023); + expect(result.start?.day).toBe(1); + expect(result.end?.month).toBe(1); + expect(result.end?.year).toBe(2024); + expect(result.end?.day).toBe(1); + }); + it('should throw if amount is undefined', () => { expect(() => resolveRelativeDateTimeFilter( diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts index 04261b4f04..def3626d90 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts @@ -28,6 +28,8 @@ export const addUnitToDateTime = ( return addWeeks(dateTime, amount); case 'MONTH': return addMonths(dateTime, amount); + case 'QUARTER': + return addMonths(dateTime, amount * 3); case 'YEAR': return addYears(dateTime, amount); } diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts index fc9dbbca35..91b0c78b9c 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts @@ -3,7 +3,7 @@ import { isNonEmptyArray } from '@sniptt/guards'; import z from 'zod'; const REGEX_FOR_RELATIVE_DATE_FILTER_STRINGIFIED_PARSING = - /((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|HOUR|MINUTE|SECOND)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/; + /((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|QUARTER|HOUR|MINUTE|SECOND)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/; export const relativeDateFilterStringifiedSchema = z .string() diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts index 09b3594252..dc61a6ae71 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts @@ -7,6 +7,7 @@ export const relativeDateFilterUnitSchema = z.enum([ 'DAY', 'WEEK', 'MONTH', + 'QUARTER', 'YEAR', ]); diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts index 5bab58f4c6..b228322703 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts @@ -19,6 +19,35 @@ export const resolveRelativeDateFilter = ( throw new Error('Amount is required'); } + if (unit === 'QUARTER') { + const startOfCurrentQuarter = getPeriodStart( + referenceTodayZonedDateTime, + 'QUARTER', + firstDayOfTheWeek, + ); + + const startOfNextPeriod = addUnitToZonedDateTime( + startOfCurrentQuarter, + 'QUARTER', + 1, + ); + + const endOfNextPeriod = addUnitToZonedDateTime( + startOfNextPeriod, + 'QUARTER', + amount, + ); + + const start = startOfNextPeriod.toPlainDate().toString(); + const end = endOfNextPeriod.toPlainDate().toString(); + + return { + ...relativeDateFilter, + start, + end, + }; + } + const startOfNextDay = referenceTodayZonedDateTime .startOfDay() .add({ days: 1 }); @@ -43,6 +72,29 @@ export const resolveRelativeDateFilter = ( throw new Error('Amount is required'); } + if (unit === 'QUARTER') { + const startOfCurrentQuarter = getPeriodStart( + referenceTodayZonedDateTime, + 'QUARTER', + firstDayOfTheWeek, + ); + + const startOfPastPeriod = subUnitFromZonedDateTime( + startOfCurrentQuarter, + 'QUARTER', + amount, + ); + + const start = startOfPastPeriod.toPlainDate().toString(); + const end = startOfCurrentQuarter.toPlainDate().toString(); + + return { + ...relativeDateFilter, + start, + end, + }; + } + const startOfDay = referenceTodayZonedDateTime.startOfDay(); const startOfNextPeriod = subUnitFromZonedDateTime( diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts index bdd563a056..6fb9748082 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts @@ -20,6 +20,19 @@ export const resolveRelativeDateTimeFilter = ( throw new Error('Amount is required'); } + if (unit === 'QUARTER') { + const startOfNextQuarter = getNextPeriodStart( + referenceZonedDateTime, + 'QUARTER', + ); + + return { + ...relativeDateFilter, + start: startOfNextQuarter, + end: addUnitToZonedDateTime(startOfNextQuarter, unit, amount), + }; + } + if (isSubDayUnit) { return { ...relativeDateFilter, @@ -43,6 +56,19 @@ export const resolveRelativeDateTimeFilter = ( throw new Error('Amount is required'); } + if (unit === 'QUARTER') { + const startOfCurrentQuarter = getPeriodStart( + referenceZonedDateTime, + 'QUARTER', + ); + + return { + ...relativeDateFilter, + start: subUnitFromZonedDateTime(startOfCurrentQuarter, unit, amount), + end: startOfCurrentQuarter, + }; + } + if (isSubDayUnit) { return { ...relativeDateFilter, diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts index 95aa9a10e7..820b26fce8 100644 --- a/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts +++ b/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts @@ -27,6 +27,8 @@ export const subUnitFromDateTime = ( return subWeeks(dateTime, amount); case 'MONTH': return subMonths(dateTime, amount); + case 'QUARTER': + return subMonths(dateTime, amount * 3); case 'YEAR': return subYears(dateTime, amount); }