mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Add calendar query prefetch
This commit is contained in:
@@ -66,7 +66,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<ReportingRow
|
||||
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 { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser';
|
||||
import { canViewClients, canViewMembers } from '@/utils/permissions';
|
||||
import {
|
||||
getInitialWeekRange,
|
||||
getExpandedCalendarDateRange,
|
||||
createCalendarQueryKey,
|
||||
fetchAllCalendarEntries,
|
||||
} from '@/utils/useTimeEntriesCalendarQuery';
|
||||
|
||||
/**
|
||||
* Route patterns mapped to their prefetch functions.
|
||||
@@ -29,6 +35,7 @@ const routePrefetchers: Record<string, (queryClient: QueryClient) => void> = {
|
||||
prefetchTasks(queryClient);
|
||||
prefetchTags(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
prefetchCalendarTimeEntries(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) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId || !canViewMembers()) return;
|
||||
|
||||
@@ -11,6 +11,7 @@ export function useOrganizationQuery(organizationId: string) {
|
||||
organization: organizationId,
|
||||
},
|
||||
}),
|
||||
staleTime: 1000 * 30,
|
||||
});
|
||||
|
||||
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 { computed, type Ref } from 'vue';
|
||||
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(
|
||||
calendarStart: Ref<Date | undefined>,
|
||||
@@ -13,68 +122,30 @@ export function useTimeEntriesCalendarQuery(
|
||||
return !!getCurrentOrganizationId() && !!calendarStart.value && !!calendarEnd.value;
|
||||
});
|
||||
|
||||
// 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 };
|
||||
}
|
||||
|
||||
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 getExpandedCalendarDateRange(calendarStart.value, calendarEnd.value);
|
||||
});
|
||||
|
||||
return useQuery<TimeEntryResponse>({
|
||||
queryKey: computed(() => [
|
||||
'timeEntries',
|
||||
'calendar',
|
||||
{
|
||||
start: expandedDateRange.value.start,
|
||||
end: expandedDateRange.value.end,
|
||||
organization: getCurrentOrganizationId(),
|
||||
},
|
||||
]),
|
||||
queryKey: computed(() =>
|
||||
createCalendarQueryKey(
|
||||
expandedDateRange.value.start,
|
||||
expandedDateRange.value.end,
|
||||
getCurrentOrganizationId()
|
||||
)
|
||||
),
|
||||
enabled: enableCalendarQuery,
|
||||
placeholderData: (previousData) => previousData,
|
||||
queryFn: async () => {
|
||||
const allEntries: TimeEntry[] = [];
|
||||
|
||||
while (true) {
|
||||
const response = await api.getTimeEntries({
|
||||
params: {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
return fetchAllCalendarEntries(
|
||||
getCurrentOrganizationId() || '',
|
||||
getCurrentMembershipId(),
|
||||
expandedDateRange.value.start!,
|
||||
expandedDateRange.value.end!
|
||||
);
|
||||
},
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user