diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange.ts index a294b60fe2..f8fd2ab64a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange.ts @@ -45,7 +45,7 @@ export const generateDateGroupsInRange = ({ break; case ObjectRecordGroupByDateGranularity.WEEK: - currentDateCursor.setDate(currentDateCursor.getDate() + 1); + currentDateCursor.setDate(currentDateCursor.getDate() + 7); break; case ObjectRecordGroupByDateGranularity.MONTH: diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts index 61f056ce36..52ffb804b0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts @@ -1,3 +1,4 @@ +import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { generateGroupByAggregateQuery } from '@/object-record/record-aggregate/utils/generateGroupByAggregateQuery'; import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields'; @@ -24,6 +25,8 @@ export const useGraphWidgetGroupByQuery = ({ configuration: GroupByChartConfiguration; limit?: number; }) => { + const { calendarStartDay } = useDateTimeFormat(); + const { objectMetadataItem, aggregateField, gqlOperationFilter } = useGraphWidgetQueryCommon({ objectMetadataItemId, @@ -63,6 +66,7 @@ export const useGraphWidgetGroupByQuery = ({ chartConfiguration: configuration, aggregateOperation: aggregateOperation, limit, + firstDayOfTheWeek: calendarStartDay, }) : generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem, @@ -71,6 +75,7 @@ export const useGraphWidgetGroupByQuery = ({ | LineChartConfiguration, aggregateOperation: aggregateOperation, limit, + firstDayOfTheWeek: calendarStartDay, }); const variables = { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/formatDateByGranularity.spec.ts.snap b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/formatDateByGranularity.spec.ts.snap index 5ba76e7849..6a977695ca 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/formatDateByGranularity.spec.ts.snap +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/formatDateByGranularity.spec.ts.snap @@ -16,4 +16,6 @@ exports[`formatDateByGranularity should format date for NONE granularity 1`] = ` exports[`formatDateByGranularity should format date for QUARTER granularity 1`] = `"Q1 2024"`; +exports[`formatDateByGranularity should format date for WEEK granularity 1`] = `"Mar 20, 2024 20 - 26, 2024"`; + exports[`formatDateByGranularity should format date for YEAR granularity 1`] = `"2024"`; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts index 7367f6538f..e252158827 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts @@ -1,4 +1,5 @@ import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject'; +import { CalendarStartDay } from 'twenty-shared'; import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -101,4 +102,82 @@ describe('buildGroupByFieldObject', () => { expect(result).toEqual({ status: true }); }); + + it('should include weekStartDay for WEEK granularity with MONDAY', () => { + const field = { + name: 'createdAt', + type: FieldMetadataType.DATE, + } as any; + + const result = buildGroupByFieldObject({ + field, + dateGranularity: ObjectRecordGroupByDateGranularity.WEEK, + firstDayOfTheWeek: CalendarStartDay.MONDAY, + }); + + expect(result).toEqual({ + createdAt: { + granularity: ObjectRecordGroupByDateGranularity.WEEK, + weekStartDay: 'MONDAY', + }, + }); + }); + + it('should include weekStartDay for WEEK granularity with SUNDAY', () => { + const field = { + name: 'createdAt', + type: FieldMetadataType.DATE, + } as any; + + const result = buildGroupByFieldObject({ + field, + dateGranularity: ObjectRecordGroupByDateGranularity.WEEK, + firstDayOfTheWeek: CalendarStartDay.SUNDAY, + }); + + expect(result).toEqual({ + createdAt: { + granularity: ObjectRecordGroupByDateGranularity.WEEK, + weekStartDay: 'SUNDAY', + }, + }); + }); + + it('should not include weekStartDay for WEEK granularity with SYSTEM', () => { + const field = { + name: 'createdAt', + type: FieldMetadataType.DATE, + } as any; + + const result = buildGroupByFieldObject({ + field, + dateGranularity: ObjectRecordGroupByDateGranularity.WEEK, + firstDayOfTheWeek: CalendarStartDay.SYSTEM, + }); + + expect(result).toEqual({ + createdAt: { + granularity: ObjectRecordGroupByDateGranularity.WEEK, + }, + }); + }); + + it('should not include weekStartDay for non-WEEK granularity', () => { + const field = { + name: 'createdAt', + type: FieldMetadataType.DATE, + } as any; + + const result = buildGroupByFieldObject({ + field, + dateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + firstDayOfTheWeek: CalendarStartDay.MONDAY, + }); + + expect(result).toEqual({ + createdAt: { + granularity: ObjectRecordGroupByDateGranularity.MONTH, + }, + }); + }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatDateByGranularity.spec.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatDateByGranularity.spec.ts index 0da6f267ae..d36905e09c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatDateByGranularity.spec.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatDateByGranularity.spec.ts @@ -27,12 +27,14 @@ describe('formatDateByGranularity', () => { const testCases: { granularity: | ObjectRecordGroupByDateGranularity.DAY + | ObjectRecordGroupByDateGranularity.WEEK | ObjectRecordGroupByDateGranularity.MONTH | ObjectRecordGroupByDateGranularity.QUARTER | ObjectRecordGroupByDateGranularity.YEAR | ObjectRecordGroupByDateGranularity.NONE; }[] = [ { granularity: ObjectRecordGroupByDateGranularity.DAY }, + { granularity: ObjectRecordGroupByDateGranularity.WEEK }, { granularity: ObjectRecordGroupByDateGranularity.MONTH }, { granularity: ObjectRecordGroupByDateGranularity.QUARTER }, { granularity: ObjectRecordGroupByDateGranularity.YEAR }, @@ -46,6 +48,53 @@ describe('formatDateByGranularity', () => { }, ); + describe('week calculations', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest + .spyOn(Date.prototype, 'toLocaleDateString') + .mockImplementation((_locales, options) => { + if (options?.weekday === 'long') return 'Wednesday'; + if (options?.month === 'long' && !isDefined(options?.year)) + return 'March'; + if (options?.month === 'long' && isDefined(options?.year)) + return 'March 2024'; + if (options?.month === 'short') return 'Mar 20, 2024'; + return '3/20/2024'; + }); + }); + + it('should format week within same month', () => { + const date = new Date('2024-05-06'); + const result = formatDateByGranularity( + date, + ObjectRecordGroupByDateGranularity.WEEK, + ); + expect(result).toBe('May 6 - 12, 2024'); + }); + + it('should format week crossing months', () => { + const date = new Date('2024-05-27'); + const result = formatDateByGranularity( + date, + ObjectRecordGroupByDateGranularity.WEEK, + ); + expect(result).toBe('May 27 - Jun 2, 2024'); + }); + + it('should format week crossing years', () => { + const date = new Date('2024-12-30'); + const result = formatDateByGranularity( + date, + ObjectRecordGroupByDateGranularity.WEEK, + ); + expect(result).toBe('Dec 30, 2024 - Jan 5, 2025'); + }); + }); + describe('quarter calculations', () => { const quarterTestCases: { date: Date; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts index 13c1f702f6..9bb493c05e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts @@ -3,7 +3,11 @@ import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/uti import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; -import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; +import { CalendarStartDay } from 'twenty-shared'; +import { + type FirstDayOfTheWeek, + ObjectRecordGroupByDateGranularity, +} from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -12,10 +16,12 @@ export const buildGroupByFieldObject = ({ field, subFieldName, dateGranularity, + firstDayOfTheWeek, }: { field: FieldMetadataItem; subFieldName?: string | null; dateGranularity?: ObjectRecordGroupByDateGranularity; + firstDayOfTheWeek?: number | null; }): Record> => { const isRelation = isFieldRelation(field) || isFieldMorphRelation(field); const isComposite = isCompositeFieldType(field.type); @@ -41,11 +47,22 @@ export const buildGroupByFieldObject = ({ } if (isDateField) { - return { - [field.name]: { - granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, - }, - }; + const granularity = dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY; + const result: Record = { granularity }; + + if ( + granularity === ObjectRecordGroupByDateGranularity.WEEK && + isDefined(firstDayOfTheWeek) && + firstDayOfTheWeek !== CalendarStartDay.SYSTEM + ) { + const weekStartDay = CalendarStartDay[ + firstDayOfTheWeek + ] as FirstDayOfTheWeek; + + result.weekStartDay = weekStartDay; + } + + return { [field.name]: result }; } return { [field.name]: true }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDateByGranularity.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDateByGranularity.ts index 1a1a826fc6..7812d13816 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDateByGranularity.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDateByGranularity.ts @@ -17,12 +17,32 @@ export const formatDateByGranularity = ( month: 'short', day: 'numeric', }); - case ObjectRecordGroupByDateGranularity.WEEK: - return date.toLocaleDateString(undefined, { - year: 'numeric', + case ObjectRecordGroupByDateGranularity.WEEK: { + const weekStart = new Date(date); + const weekEnd = new Date(date); + weekEnd.setDate(weekEnd.getDate() + 6); + + const startMonth = weekStart.toLocaleDateString(undefined, { month: 'short', - day: 'numeric', }); + const endMonth = weekEnd.toLocaleDateString(undefined, { + month: 'short', + }); + const startDay = weekStart.getDate(); + const endDay = weekEnd.getDate(); + const startYear = weekStart.getFullYear(); + const endYear = weekEnd.getFullYear(); + + if (startYear !== endYear) { + return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`; + } + + if (startMonth !== endMonth) { + return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${endYear}`; + } + + return `${startMonth} ${startDay} - ${endDay}, ${endYear}`; + } case ObjectRecordGroupByDateGranularity.MONTH: return date.toLocaleDateString(undefined, { year: 'numeric', diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts index dd4dec393c..a9d901a7f5 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts @@ -18,11 +18,13 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ chartConfiguration, aggregateOperation, limit, + firstDayOfTheWeek, }: { objectMetadataItem: ObjectMetadataItem; chartConfiguration: BarChartConfiguration | LineChartConfiguration; aggregateOperation?: string; limit?: number; + firstDayOfTheWeek?: number; }) => { const groupByFieldXId = chartConfiguration.primaryAxisGroupByFieldMetadataId; @@ -59,6 +61,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ subFieldName: groupBySubFieldNameX, dateGranularity: chartConfiguration.primaryAxisDateGranularity ?? undefined, + firstDayOfTheWeek, }), ); @@ -69,6 +72,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ subFieldName: groupBySubFieldNameY, dateGranularity: chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined, + firstDayOfTheWeek, }), ); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts index 235d1673d1..24848ad402 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts @@ -15,11 +15,13 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ chartConfiguration, aggregateOperation, limit, + firstDayOfTheWeek, }: { objectMetadataItem: ObjectMetadataItem; chartConfiguration: PieChartConfiguration; aggregateOperation?: string; limit?: number; + firstDayOfTheWeek?: number; }) => { const groupByFieldId = chartConfiguration.groupByFieldMetadataId; const groupBySubFieldName = @@ -43,6 +45,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ field: groupByField, subFieldName: groupBySubFieldName, dateGranularity, + firstDayOfTheWeek, }), ];