Add calendar query prefetch

This commit is contained in:
Gregor Vostrak
2026-02-03 14:12:36 +01:00
parent 28fc324c6a
commit 537a023ab9
4 changed files with 149 additions and 54 deletions

View File

@@ -66,7 +66,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
</div> </div>
<div <div
v-if="expanded && entry.grouped_data" v-if="expanded && entry.grouped_data"
class="col-span-3 grid bg-quaternary" class="col-span-3 grid bg-tertiary"
style="grid-template-columns: 1fr 150px 150px"> style="grid-template-columns: 1fr 150px 150px">
<ReportingRow <ReportingRow
v-for="subEntry in entry.grouped_data" v-for="subEntry in entry.grouped_data"

View File

@@ -2,6 +2,12 @@ import type { QueryClient } from '@tanstack/vue-query';
import { api } from '@/packages/api/src'; import { api } from '@/packages/api/src';
import { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser'; import { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser';
import { canViewClients, canViewMembers } from '@/utils/permissions'; import { canViewClients, canViewMembers } from '@/utils/permissions';
import {
getInitialWeekRange,
getExpandedCalendarDateRange,
createCalendarQueryKey,
fetchAllCalendarEntries,
} from '@/utils/useTimeEntriesCalendarQuery';
/** /**
* Route patterns mapped to their prefetch functions. * Route patterns mapped to their prefetch functions.
@@ -29,6 +35,7 @@ const routePrefetchers: Record<string, (queryClient: QueryClient) => void> = {
prefetchTasks(queryClient); prefetchTasks(queryClient);
prefetchTags(queryClient); prefetchTags(queryClient);
prefetchClients(queryClient); prefetchClients(queryClient);
prefetchCalendarTimeEntries(queryClient);
}, },
'/projects': (queryClient) => { '/projects': (queryClient) => {
@@ -244,6 +251,22 @@ function prefetchTimeEntries(queryClient: QueryClient) {
}); });
} }
function prefetchCalendarTimeEntries(queryClient: QueryClient) {
const organizationId = getCurrentOrganizationId();
const memberId = getCurrentMembershipId();
if (!organizationId) return;
const { start, end } = getInitialWeekRange();
const { start: formattedStart, end: formattedEnd } = getExpandedCalendarDateRange(start, end);
queryClient.prefetchQuery({
queryKey: createCalendarQueryKey(formattedStart, formattedEnd, organizationId),
queryFn: () =>
fetchAllCalendarEntries(organizationId, memberId, formattedStart, formattedEnd),
staleTime: 30000,
});
}
function prefetchProjectMembers(queryClient: QueryClient, projectId: string) { function prefetchProjectMembers(queryClient: QueryClient, projectId: string) {
const organizationId = getCurrentOrganizationId(); const organizationId = getCurrentOrganizationId();
if (!organizationId || !canViewMembers()) return; if (!organizationId || !canViewMembers()) return;

View File

@@ -11,6 +11,7 @@ export function useOrganizationQuery(organizationId: string) {
organization: organizationId, organization: organizationId,
}, },
}), }),
staleTime: 1000 * 30,
}); });
const organization = computed(() => query.data.value?.data); const organization = computed(() => query.data.value?.data);

View File

@@ -3,64 +3,101 @@ import { api, type TimeEntryResponse, type TimeEntry } from '@/packages/api/src'
import { getCurrentMembershipId, getCurrentOrganizationId } from '@/utils/useUser'; import { getCurrentMembershipId, getCurrentOrganizationId } from '@/utils/useUser';
import { computed, type Ref } from 'vue'; import { computed, type Ref } from 'vue';
import { getDayJsInstance } from '@/packages/ui/src/utils/time'; import { getDayJsInstance } from '@/packages/ui/src/utils/time';
import { getUserTimezone } from '@/packages/ui/src/utils/settings'; import { getUserTimezone, getWeekStart } from '@/packages/ui/src/utils/settings';
export function useTimeEntriesCalendarQuery( const weekStartMap: Record<string, number> = {
calendarStart: Ref<Date | undefined>, sunday: 0,
calendarEnd: Ref<Date | undefined> monday: 1,
) { tuesday: 2,
const enableCalendarQuery = computed(() => { wednesday: 3,
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value; thursday: 4,
}); friday: 5,
saturday: 6,
// Calculate expanded date range to include previous and next periods with timezone transformations };
const expandedDateRange = computed(() => {
if (!calendarStart.value || !calendarEnd.value) {
return { start: null, end: null };
}
/**
* Calculate expanded date range to include previous and next periods with timezone transformations.
* This allows smooth navigation between calendar views without loading delays.
*/
export function getExpandedCalendarDateRange(
calendarStart: Date,
calendarEnd: Date
): { start: string; end: string } {
const dayjs = getDayJsInstance(); const dayjs = getDayJsInstance();
const duration = dayjs(calendarEnd.value).diff(dayjs(calendarStart.value), 'milliseconds'); const duration = dayjs(calendarEnd).diff(dayjs(calendarStart), 'milliseconds');
// Calculate previous period // Calculate previous period
const previousStart = dayjs(calendarStart.value).subtract(duration, 'milliseconds'); const previousStart = dayjs(calendarStart).subtract(duration, 'milliseconds');
// Calculate next period // Calculate next period
const nextEnd = dayjs(calendarEnd.value).add(duration, 'milliseconds'); const nextEnd = dayjs(calendarEnd).add(duration, 'milliseconds');
// Apply timezone transformations // Apply timezone transformations
const formattedStart = previousStart.utc().tz(getUserTimezone(), true).utc().format(); const timezone = getUserTimezone();
const formattedEnd = nextEnd.utc().tz(getUserTimezone(), true).utc().format(); const formattedStart = previousStart.utc().tz(timezone, true).utc().format();
const formattedEnd = nextEnd.utc().tz(timezone, true).utc().format();
return { return {
start: formattedStart, start: formattedStart,
end: formattedEnd, end: formattedEnd,
}; };
}); }
return useQuery<TimeEntryResponse>({ /**
queryKey: computed(() => [ * Get the initial week view date range based on user's week start preference.
* Matches FullCalendar's timeGridWeek initial view.
*/
export function getInitialWeekRange(): { start: Date; end: Date } {
const dayjs = getDayJsInstance();
const weekStart = getWeekStart();
const firstDay = weekStartMap[weekStart] ?? 1;
const now = dayjs();
const currentDayOfWeek = now.day();
const daysFromWeekStart = (currentDayOfWeek - firstDay + 7) % 7;
const calendarStart = now.subtract(daysFromWeekStart, 'day').startOf('day');
const calendarEnd = calendarStart.add(7, 'day');
return {
start: calendarStart.toDate(),
end: calendarEnd.toDate(),
};
}
/**
* Create the query key for calendar time entries.
*/
export function createCalendarQueryKey(
start: string | null,
end: string | null,
organizationId: string | null
): readonly [
'timeEntries', 'timeEntries',
'calendar', 'calendar',
{ { start: string | null; end: string | null; organization: string | null },
start: expandedDateRange.value.start, ] {
end: expandedDateRange.value.end, return ['timeEntries', 'calendar', { start, end, organization: organizationId }] as const;
organization: getCurrentOrganizationId(), }
},
]), /**
enabled: enableCalendarQuery, * Fetch all calendar entries with pagination.
placeholderData: (previousData) => previousData, */
queryFn: async () => { export async function fetchAllCalendarEntries(
organizationId: string,
memberId: string | undefined,
start: string,
end: string
): Promise<TimeEntryResponse> {
const allEntries: TimeEntry[] = []; const allEntries: TimeEntry[] = [];
while (true) { while (true) {
const response = await api.getTimeEntries({ const response = await api.getTimeEntries({
params: { params: {
organization: getCurrentOrganizationId() || '', organization: organizationId,
}, },
queries: { queries: {
start: expandedDateRange.value.start!, start,
end: expandedDateRange.value.end!, end,
member_id: getCurrentMembershipId(), member_id: memberId,
offset: allEntries.length || undefined, offset: allEntries.length || undefined,
}, },
}); });
@@ -75,6 +112,40 @@ export function useTimeEntriesCalendarQuery(
return { data: allEntries, meta: response.meta }; return { data: allEntries, meta: response.meta };
} }
} }
}
export function useTimeEntriesCalendarQuery(
calendarStart: Ref<Date | undefined>,
calendarEnd: Ref<Date | undefined>
) {
const enableCalendarQuery = computed(() => {
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value;
});
const expandedDateRange = computed(() => {
if (!calendarStart.value || !calendarEnd.value) {
return { start: null, end: null };
}
return getExpandedCalendarDateRange(calendarStart.value, calendarEnd.value);
});
return useQuery<TimeEntryResponse>({
queryKey: computed(() =>
createCalendarQueryKey(
expandedDateRange.value.start,
expandedDateRange.value.end,
getCurrentOrganizationId()
)
),
enabled: enableCalendarQuery,
placeholderData: (previousData) => previousData,
queryFn: async () => {
return fetchAllCalendarEntries(
getCurrentOrganizationId() || '',
getCurrentMembershipId(),
expandedDateRange.value.start!,
expandedDateRange.value.end!
);
}, },
staleTime: 1000 * 30, // 30 seconds staleTime: 1000 * 30, // 30 seconds
}); });