mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +01:00
Add calendar query prefetch
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -3,7 +3,116 @@ 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';
|
||||||
|
|
||||||
|
const weekStartMap: Record<string, number> = {
|
||||||
|
sunday: 0,
|
||||||
|
monday: 1,
|
||||||
|
tuesday: 2,
|
||||||
|
wednesday: 3,
|
||||||
|
thursday: 4,
|
||||||
|
friday: 5,
|
||||||
|
saturday: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 duration = dayjs(calendarEnd).diff(dayjs(calendarStart), 'milliseconds');
|
||||||
|
|
||||||
|
// Calculate previous period
|
||||||
|
const previousStart = dayjs(calendarStart).subtract(duration, 'milliseconds');
|
||||||
|
// Calculate next period
|
||||||
|
const nextEnd = dayjs(calendarEnd).add(duration, 'milliseconds');
|
||||||
|
|
||||||
|
// Apply timezone transformations
|
||||||
|
const timezone = getUserTimezone();
|
||||||
|
const formattedStart = previousStart.utc().tz(timezone, true).utc().format();
|
||||||
|
const formattedEnd = nextEnd.utc().tz(timezone, true).utc().format();
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: formattedStart,
|
||||||
|
end: formattedEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
'calendar',
|
||||||
|
{ start: string | null; end: string | null; organization: string | null },
|
||||||
|
] {
|
||||||
|
return ['timeEntries', 'calendar', { start, end, organization: organizationId }] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all calendar entries with pagination.
|
||||||
|
*/
|
||||||
|
export async function fetchAllCalendarEntries(
|
||||||
|
organizationId: string,
|
||||||
|
memberId: string | undefined,
|
||||||
|
start: string,
|
||||||
|
end: string
|
||||||
|
): Promise<TimeEntryResponse> {
|
||||||
|
const allEntries: TimeEntry[] = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const response = await api.getTimeEntries({
|
||||||
|
params: {
|
||||||
|
organization: organizationId,
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
member_id: memberId,
|
||||||
|
offset: allEntries.length || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.length === 0) {
|
||||||
|
return { data: allEntries, meta: response.meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries.push(...response.data);
|
||||||
|
|
||||||
|
if (allEntries.length >= response.meta.total) {
|
||||||
|
return { data: allEntries, meta: response.meta };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useTimeEntriesCalendarQuery(
|
export function useTimeEntriesCalendarQuery(
|
||||||
calendarStart: Ref<Date | undefined>,
|
calendarStart: Ref<Date | undefined>,
|
||||||
@@ -13,68 +122,30 @@ export function useTimeEntriesCalendarQuery(
|
|||||||
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value;
|
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate expanded date range to include previous and next periods with timezone transformations
|
|
||||||
const expandedDateRange = computed(() => {
|
const expandedDateRange = computed(() => {
|
||||||
if (!calendarStart.value || !calendarEnd.value) {
|
if (!calendarStart.value || !calendarEnd.value) {
|
||||||
return { start: null, end: null };
|
return { start: null, end: null };
|
||||||
}
|
}
|
||||||
|
return getExpandedCalendarDateRange(calendarStart.value, calendarEnd.value);
|
||||||
const dayjs = getDayJsInstance();
|
|
||||||
const duration = dayjs(calendarEnd.value).diff(dayjs(calendarStart.value), 'milliseconds');
|
|
||||||
|
|
||||||
// Calculate previous period
|
|
||||||
const previousStart = dayjs(calendarStart.value).subtract(duration, 'milliseconds');
|
|
||||||
// Calculate next period
|
|
||||||
const nextEnd = dayjs(calendarEnd.value).add(duration, 'milliseconds');
|
|
||||||
|
|
||||||
// Apply timezone transformations
|
|
||||||
const formattedStart = previousStart.utc().tz(getUserTimezone(), true).utc().format();
|
|
||||||
const formattedEnd = nextEnd.utc().tz(getUserTimezone(), true).utc().format();
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: formattedStart,
|
|
||||||
end: formattedEnd,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return useQuery<TimeEntryResponse>({
|
return useQuery<TimeEntryResponse>({
|
||||||
queryKey: computed(() => [
|
queryKey: computed(() =>
|
||||||
'timeEntries',
|
createCalendarQueryKey(
|
||||||
'calendar',
|
expandedDateRange.value.start,
|
||||||
{
|
expandedDateRange.value.end,
|
||||||
start: expandedDateRange.value.start,
|
getCurrentOrganizationId()
|
||||||
end: expandedDateRange.value.end,
|
)
|
||||||
organization: getCurrentOrganizationId(),
|
),
|
||||||
},
|
|
||||||
]),
|
|
||||||
enabled: enableCalendarQuery,
|
enabled: enableCalendarQuery,
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const allEntries: TimeEntry[] = [];
|
return fetchAllCalendarEntries(
|
||||||
|
getCurrentOrganizationId() || '',
|
||||||
while (true) {
|
getCurrentMembershipId(),
|
||||||
const response = await api.getTimeEntries({
|
expandedDateRange.value.start!,
|
||||||
params: {
|
expandedDateRange.value.end!
|
||||||
organization: getCurrentOrganizationId() || '',
|
);
|
||||||
},
|
|
||||||
queries: {
|
|
||||||
start: expandedDateRange.value.start!,
|
|
||||||
end: expandedDateRange.value.end!,
|
|
||||||
member_id: getCurrentMembershipId(),
|
|
||||||
offset: allEntries.length || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.length === 0) {
|
|
||||||
return { data: allEntries, meta: response.meta };
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries.push(...response.data);
|
|
||||||
|
|
||||||
if (allEntries.length >= response.meta.total) {
|
|
||||||
return { data: allEntries, meta: response.meta };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 30, // 30 seconds
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user