Skip to content

Commit a58becc

Browse files
committed
Add calendar query prefetch
1 parent 09c3205 commit a58becc

File tree

4 files changed

+149
-54
lines changed

4 files changed

+149
-54
lines changed

resources/js/Components/Common/Reporting/ReportingRow.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
6666
</div>
6767
<div
6868
v-if="expanded && entry.grouped_data"
69-
class="col-span-3 grid bg-quaternary"
69+
class="col-span-3 grid bg-tertiary"
7070
style="grid-template-columns: 1fr 150px 150px">
7171
<ReportingRow
7272
v-for="subEntry in entry.grouped_data"

resources/js/utils/prefetch.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import type { QueryClient } from '@tanstack/vue-query';
22
import { api } from '@/packages/api/src';
33
import { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser';
44
import { canViewClients, canViewMembers } from '@/utils/permissions';
5+
import {
6+
getInitialWeekRange,
7+
getExpandedCalendarDateRange,
8+
createCalendarQueryKey,
9+
fetchAllCalendarEntries,
10+
} from '@/utils/useTimeEntriesCalendarQuery';
511

612
/**
713
* Route patterns mapped to their prefetch functions.
@@ -29,6 +35,7 @@ const routePrefetchers: Record<string, (queryClient: QueryClient) => void> = {
2935
prefetchTasks(queryClient);
3036
prefetchTags(queryClient);
3137
prefetchClients(queryClient);
38+
prefetchCalendarTimeEntries(queryClient);
3239
},
3340

3441
'/projects': (queryClient) => {
@@ -244,6 +251,22 @@ function prefetchTimeEntries(queryClient: QueryClient) {
244251
});
245252
}
246253

254+
function prefetchCalendarTimeEntries(queryClient: QueryClient) {
255+
const organizationId = getCurrentOrganizationId();
256+
const memberId = getCurrentMembershipId();
257+
if (!organizationId) return;
258+
259+
const { start, end } = getInitialWeekRange();
260+
const { start: formattedStart, end: formattedEnd } = getExpandedCalendarDateRange(start, end);
261+
262+
queryClient.prefetchQuery({
263+
queryKey: createCalendarQueryKey(formattedStart, formattedEnd, organizationId),
264+
queryFn: () =>
265+
fetchAllCalendarEntries(organizationId, memberId, formattedStart, formattedEnd),
266+
staleTime: 30000,
267+
});
268+
}
269+
247270
function prefetchProjectMembers(queryClient: QueryClient, projectId: string) {
248271
const organizationId = getCurrentOrganizationId();
249272
if (!organizationId || !canViewMembers()) return;

resources/js/utils/useOrganizationQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function useOrganizationQuery(organizationId: string) {
1111
organization: organizationId,
1212
},
1313
}),
14+
staleTime: 1000 * 30,
1415
});
1516

1617
const organization = computed(() => query.data.value?.data);

resources/js/utils/useTimeEntriesCalendarQuery.ts

Lines changed: 124 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,116 @@ import { api, type TimeEntryResponse, type TimeEntry } from '@/packages/api/src'
33
import { getCurrentMembershipId, getCurrentOrganizationId } from '@/utils/useUser';
44
import { computed, type Ref } from 'vue';
55
import { getDayJsInstance } from '@/packages/ui/src/utils/time';
6-
import { getUserTimezone } from '@/packages/ui/src/utils/settings';
6+
import { getUserTimezone, getWeekStart } from '@/packages/ui/src/utils/settings';
7+
8+
const weekStartMap: Record<string, number> = {
9+
sunday: 0,
10+
monday: 1,
11+
tuesday: 2,
12+
wednesday: 3,
13+
thursday: 4,
14+
friday: 5,
15+
saturday: 6,
16+
};
17+
18+
/**
19+
* Calculate expanded date range to include previous and next periods with timezone transformations.
20+
* This allows smooth navigation between calendar views without loading delays.
21+
*/
22+
export function getExpandedCalendarDateRange(
23+
calendarStart: Date,
24+
calendarEnd: Date
25+
): { start: string; end: string } {
26+
const dayjs = getDayJsInstance();
27+
const duration = dayjs(calendarEnd).diff(dayjs(calendarStart), 'milliseconds');
28+
29+
// Calculate previous period
30+
const previousStart = dayjs(calendarStart).subtract(duration, 'milliseconds');
31+
// Calculate next period
32+
const nextEnd = dayjs(calendarEnd).add(duration, 'milliseconds');
33+
34+
// Apply timezone transformations
35+
const timezone = getUserTimezone();
36+
const formattedStart = previousStart.utc().tz(timezone, true).utc().format();
37+
const formattedEnd = nextEnd.utc().tz(timezone, true).utc().format();
38+
39+
return {
40+
start: formattedStart,
41+
end: formattedEnd,
42+
};
43+
}
44+
45+
/**
46+
* Get the initial week view date range based on user's week start preference.
47+
* Matches FullCalendar's timeGridWeek initial view.
48+
*/
49+
export function getInitialWeekRange(): { start: Date; end: Date } {
50+
const dayjs = getDayJsInstance();
51+
const weekStart = getWeekStart();
52+
const firstDay = weekStartMap[weekStart] ?? 1;
53+
54+
const now = dayjs();
55+
const currentDayOfWeek = now.day();
56+
const daysFromWeekStart = (currentDayOfWeek - firstDay + 7) % 7;
57+
const calendarStart = now.subtract(daysFromWeekStart, 'day').startOf('day');
58+
const calendarEnd = calendarStart.add(7, 'day');
59+
60+
return {
61+
start: calendarStart.toDate(),
62+
end: calendarEnd.toDate(),
63+
};
64+
}
65+
66+
/**
67+
* Create the query key for calendar time entries.
68+
*/
69+
export function createCalendarQueryKey(
70+
start: string | null,
71+
end: string | null,
72+
organizationId: string | null
73+
): readonly [
74+
'timeEntries',
75+
'calendar',
76+
{ start: string | null; end: string | null; organization: string | null },
77+
] {
78+
return ['timeEntries', 'calendar', { start, end, organization: organizationId }] as const;
79+
}
80+
81+
/**
82+
* Fetch all calendar entries with pagination.
83+
*/
84+
export async function fetchAllCalendarEntries(
85+
organizationId: string,
86+
memberId: string | undefined,
87+
start: string,
88+
end: string
89+
): Promise<TimeEntryResponse> {
90+
const allEntries: TimeEntry[] = [];
91+
92+
while (true) {
93+
const response = await api.getTimeEntries({
94+
params: {
95+
organization: organizationId,
96+
},
97+
queries: {
98+
start,
99+
end,
100+
member_id: memberId,
101+
offset: allEntries.length || undefined,
102+
},
103+
});
104+
105+
if (response.data.length === 0) {
106+
return { data: allEntries, meta: response.meta };
107+
}
108+
109+
allEntries.push(...response.data);
110+
111+
if (allEntries.length >= response.meta.total) {
112+
return { data: allEntries, meta: response.meta };
113+
}
114+
}
115+
}
7116

8117
export function useTimeEntriesCalendarQuery(
9118
calendarStart: Ref<Date | undefined>,
@@ -13,68 +122,30 @@ export function useTimeEntriesCalendarQuery(
13122
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value;
14123
});
15124

16-
// Calculate expanded date range to include previous and next periods with timezone transformations
17125
const expandedDateRange = computed(() => {
18126
if (!calendarStart.value || !calendarEnd.value) {
19127
return { start: null, end: null };
20128
}
21-
22-
const dayjs = getDayJsInstance();
23-
const duration = dayjs(calendarEnd.value).diff(dayjs(calendarStart.value), 'milliseconds');
24-
25-
// Calculate previous period
26-
const previousStart = dayjs(calendarStart.value).subtract(duration, 'milliseconds');
27-
// Calculate next period
28-
const nextEnd = dayjs(calendarEnd.value).add(duration, 'milliseconds');
29-
30-
// Apply timezone transformations
31-
const formattedStart = previousStart.utc().tz(getUserTimezone(), true).utc().format();
32-
const formattedEnd = nextEnd.utc().tz(getUserTimezone(), true).utc().format();
33-
34-
return {
35-
start: formattedStart,
36-
end: formattedEnd,
37-
};
129+
return getExpandedCalendarDateRange(calendarStart.value, calendarEnd.value);
38130
});
39131

40132
return useQuery<TimeEntryResponse>({
41-
queryKey: computed(() => [
42-
'timeEntries',
43-
'calendar',
44-
{
45-
start: expandedDateRange.value.start,
46-
end: expandedDateRange.value.end,
47-
organization: getCurrentOrganizationId(),
48-
},
49-
]),
133+
queryKey: computed(() =>
134+
createCalendarQueryKey(
135+
expandedDateRange.value.start,
136+
expandedDateRange.value.end,
137+
getCurrentOrganizationId()
138+
)
139+
),
50140
enabled: enableCalendarQuery,
51141
placeholderData: (previousData) => previousData,
52142
queryFn: async () => {
53-
const allEntries: TimeEntry[] = [];
54-
55-
while (true) {
56-
const response = await api.getTimeEntries({
57-
params: {
58-
organization: getCurrentOrganizationId() || '',
59-
},
60-
queries: {
61-
start: expandedDateRange.value.start!,
62-
end: expandedDateRange.value.end!,
63-
member_id: getCurrentMembershipId(),
64-
offset: allEntries.length || undefined,
65-
},
66-
});
67-
68-
if (response.data.length === 0) {
69-
return { data: allEntries, meta: response.meta };
70-
}
71-
72-
allEntries.push(...response.data);
73-
74-
if (allEntries.length >= response.meta.total) {
75-
return { data: allEntries, meta: response.meta };
76-
}
77-
}
143+
return fetchAllCalendarEntries(
144+
getCurrentOrganizationId() || '',
145+
getCurrentMembershipId(),
146+
expandedDateRange.value.start!,
147+
expandedDateRange.value.end!
148+
);
78149
},
79150
staleTime: 1000 * 30, // 30 seconds
80151
});

0 commit comments

Comments
 (0)