mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
upgrade inertia v2; add prefetching; migrate queries to tanstack query
vue
This commit is contained in:
@@ -1,26 +1,9 @@
|
||||
import { useProjectsStore } from '@/utils/useProjects';
|
||||
import { useTasksStore } from '@/utils/useTasks';
|
||||
import { useTagsStore } from '@/utils/useTags';
|
||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { useMembersStore } from '@/utils/useMembers';
|
||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||
import { canViewClients, canViewMembers } from '@/utils/permissions';
|
||||
|
||||
export function initializeStores() {
|
||||
refreshStores();
|
||||
}
|
||||
|
||||
export function refreshStores() {
|
||||
useProjectsStore().fetchProjects();
|
||||
useTasksStore().fetchTasks();
|
||||
useTagsStore().fetchTags();
|
||||
// TanStack Query now handles projects, tasks, tags, clients, and members fetching automatically
|
||||
// Only initialize stores that aren't migrated to TanStack Query yet
|
||||
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
|
||||
useTimeEntriesStore().patchTimeEntries();
|
||||
if (canViewMembers()) {
|
||||
useMembersStore().fetchMembers();
|
||||
}
|
||||
if (canViewClients()) {
|
||||
useClientsStore().fetchClients();
|
||||
}
|
||||
}
|
||||
|
||||
283
resources/js/utils/prefetch.ts
Normal file
283
resources/js/utils/prefetch.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import type { QueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { canViewClients, canViewMembers } from '@/utils/permissions';
|
||||
|
||||
/**
|
||||
* Route patterns mapped to their prefetch functions.
|
||||
* Each function receives the QueryClient and prefetches relevant data.
|
||||
*/
|
||||
const routePrefetchers: Record<string, (queryClient: QueryClient) => void> = {
|
||||
'/': (queryClient) => {
|
||||
prefetchDashboard(queryClient);
|
||||
},
|
||||
|
||||
'/dashboard': (queryClient) => {
|
||||
prefetchDashboard(queryClient);
|
||||
},
|
||||
|
||||
'/time': (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchTasks(queryClient);
|
||||
prefetchTags(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
},
|
||||
|
||||
'/calendar': (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchTasks(queryClient);
|
||||
prefetchTags(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
},
|
||||
|
||||
'/projects': (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
},
|
||||
|
||||
'/clients': (queryClient) => {
|
||||
prefetchClients(queryClient);
|
||||
},
|
||||
|
||||
'/tags': (queryClient) => {
|
||||
prefetchTags(queryClient);
|
||||
},
|
||||
|
||||
'/members': (queryClient) => {
|
||||
prefetchMembers(queryClient);
|
||||
},
|
||||
|
||||
'/reporting': (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchTags(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
prefetchMembers(queryClient);
|
||||
},
|
||||
|
||||
'/reporting/detailed': (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchTasks(queryClient);
|
||||
prefetchTags(queryClient);
|
||||
prefetchClients(queryClient);
|
||||
prefetchMembers(queryClient);
|
||||
},
|
||||
|
||||
'/reporting/shared': (queryClient) => {
|
||||
prefetchReports(queryClient);
|
||||
},
|
||||
};
|
||||
|
||||
function prefetchDashboard(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) return;
|
||||
|
||||
// Prefetch all dashboard card data
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['timeEntries', organizationId],
|
||||
queryFn: () =>
|
||||
api.getTimeEntries({
|
||||
params: { organization: organizationId },
|
||||
queries: { limit: 10, offset: 0, only_full_dates: 'true' },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['lastSevenDays', organizationId],
|
||||
queryFn: () => api.lastSevenDays({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['dailyTrackedHours', organizationId],
|
||||
queryFn: () => api.dailyTrackedHours({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['weeklyProjectOverview', organizationId],
|
||||
queryFn: () => api.weeklyProjectOverview({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['totalWeeklyTime', organizationId],
|
||||
queryFn: () => api.totalWeeklyTime({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['totalWeeklyBillableTime', organizationId],
|
||||
queryFn: () => api.totalWeeklyBillableTime({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['totalWeeklyBillableAmount', organizationId],
|
||||
queryFn: () => api.totalWeeklyBillableAmount({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['weeklyHistory', organizationId],
|
||||
queryFn: () => api.weeklyHistory({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
// Prefetch team activity only if user has permission
|
||||
if (canViewMembers()) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['latestTeamActivity', organizationId],
|
||||
queryFn: () => api.latestTeamActivity({ params: { organization: organizationId } }),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function prefetchProjects(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['projects'],
|
||||
queryFn: () =>
|
||||
api.getProjects({
|
||||
params: { organization: organizationId },
|
||||
queries: { archived: 'all' },
|
||||
}),
|
||||
staleTime: 30000, // Consider fresh for 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchTasks(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['tasks'],
|
||||
queryFn: () =>
|
||||
api.getTasks({
|
||||
params: { organization: organizationId },
|
||||
queries: { done: 'all' },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchTags(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['tags'],
|
||||
queryFn: () =>
|
||||
api.getTags({
|
||||
params: { organization: organizationId },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchClients(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId || !canViewClients()) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['clients'],
|
||||
queryFn: () =>
|
||||
api.getClients({
|
||||
params: { organization: organizationId },
|
||||
queries: { archived: 'all' },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchMembers(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId || !canViewMembers()) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['members'],
|
||||
queryFn: () =>
|
||||
api.getMembers({
|
||||
params: { organization: organizationId },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchReports(queryClient: QueryClient) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['reports', 1],
|
||||
queryFn: () =>
|
||||
api.getReports({
|
||||
params: { organization: organizationId },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchProjectMembers(queryClient: QueryClient, projectId: string) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId || !canViewMembers()) return;
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['projectMembers', projectId],
|
||||
queryFn: () =>
|
||||
api.getProjectMembers({
|
||||
params: { organization: organizationId, project: projectId },
|
||||
}),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a URL to find the appropriate prefetcher.
|
||||
* Handles both exact matches and pattern matching for dynamic routes.
|
||||
*/
|
||||
function findPrefetcher(url: string): ((queryClient: QueryClient) => void) | undefined {
|
||||
// Extract pathname from URL
|
||||
const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0];
|
||||
|
||||
// Try exact match first
|
||||
if (routePrefetchers[pathname]) {
|
||||
return routePrefetchers[pathname];
|
||||
}
|
||||
|
||||
// Try pattern matching for dynamic routes like /projects/{id}
|
||||
const projectMatch = pathname.match(/^\/projects\/([^/]+)$/);
|
||||
if (projectMatch) {
|
||||
const projectId = projectMatch[1];
|
||||
return (queryClient) => {
|
||||
prefetchProjects(queryClient);
|
||||
prefetchTasks(queryClient);
|
||||
prefetchProjectMembers(queryClient, projectId);
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Inertia prefetch event listener to warm TanStack Query cache.
|
||||
* Call this once during app initialization.
|
||||
*/
|
||||
export function setupPrefetching(queryClient: QueryClient) {
|
||||
// Listen for the 'prefetching' event which fires when Inertia starts prefetching a page
|
||||
// The event detail contains the visit object with the URL being prefetched
|
||||
document.addEventListener('inertia:prefetching', ((event: CustomEvent) => {
|
||||
const visit = event.detail?.visit;
|
||||
if (!visit?.url) return;
|
||||
|
||||
const url = visit.url.href || visit.url.toString();
|
||||
const prefetcher = findPrefetcher(url);
|
||||
|
||||
if (prefetcher) {
|
||||
prefetcher(queryClient);
|
||||
}
|
||||
}) as EventListener);
|
||||
}
|
||||
@@ -1,37 +1,13 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { computed, ref } from 'vue';
|
||||
import type {
|
||||
CreateClientBody,
|
||||
ClientIndexResponse,
|
||||
Client,
|
||||
UpdateClientBody,
|
||||
} from '@/packages/api/src';
|
||||
import type { CreateClientBody, Client, UpdateClientBody } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export const useClientsStore = defineStore('clients', () => {
|
||||
const clientResponse = ref<ClientIndexResponse | null>(null);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
async function fetchClients() {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
clientResponse.value = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.getClients({
|
||||
queries: {
|
||||
archived: 'all',
|
||||
},
|
||||
params: {
|
||||
organization: organization,
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
'Failed to fetch clients'
|
||||
);
|
||||
}
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function createClient(clientBody: CreateClientBody): Promise<Client | undefined> {
|
||||
const organization = getCurrentOrganizationId();
|
||||
@@ -46,7 +22,7 @@ export const useClientsStore = defineStore('clients', () => {
|
||||
'Client created successfully',
|
||||
'Failed to create client'
|
||||
);
|
||||
await fetchClients();
|
||||
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||
return response?.data;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +41,7 @@ export const useClientsStore = defineStore('clients', () => {
|
||||
'Client updated successfully',
|
||||
'Failed to update client'
|
||||
);
|
||||
await fetchClients();
|
||||
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,13 +59,9 @@ export const useClientsStore = defineStore('clients', () => {
|
||||
'Client deleted successfully',
|
||||
'Failed to delete client'
|
||||
);
|
||||
await fetchClients();
|
||||
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||
}
|
||||
}
|
||||
|
||||
const clients = computed<Client[]>(() => {
|
||||
return clientResponse.value?.data || [];
|
||||
});
|
||||
|
||||
return { clients, fetchClients, createClient, deleteClient, updateClient };
|
||||
return { createClient, deleteClient, updateClient };
|
||||
});
|
||||
|
||||
35
resources/js/utils/useClientsQuery.ts
Normal file
35
resources/js/utils/useClientsQuery.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { Client } from '@/packages/api/src';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useClientsQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['clients'],
|
||||
queryFn: async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) throw new Error('No organization');
|
||||
return api.getClients({
|
||||
params: { organization: organizationId },
|
||||
queries: { archived: 'all' },
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId(),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const clients = computed<Client[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
const invalidateClients = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
clients,
|
||||
invalidateClients,
|
||||
};
|
||||
}
|
||||
@@ -1,31 +1,15 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Member, MemberIndexResponse, UpdateMemberBody } from '@/packages/api/src';
|
||||
import type { UpdateMemberBody } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export type MemberBillableKey = 'default-rate' | 'custom-rate';
|
||||
|
||||
export const useMembersStore = defineStore('members', () => {
|
||||
const membersResponse = ref<MemberIndexResponse | null>(null);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
async function fetchMembers() {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
membersResponse.value = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.getMembers({
|
||||
params: {
|
||||
organization: organization,
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
'Failed to fetch members'
|
||||
);
|
||||
}
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function removeMember(membershipId: string) {
|
||||
const organization = getCurrentOrganizationId();
|
||||
@@ -41,7 +25,7 @@ export const useMembersStore = defineStore('members', () => {
|
||||
'Member deleted successfully',
|
||||
'Failed to delete member'
|
||||
);
|
||||
await fetchMembers();
|
||||
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +43,9 @@ export const useMembersStore = defineStore('members', () => {
|
||||
'Member updated successfully',
|
||||
'Failed to update member'
|
||||
);
|
||||
await fetchMembers();
|
||||
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||
}
|
||||
}
|
||||
|
||||
const members = computed<Member[]>(() => {
|
||||
return membersResponse.value?.data || [];
|
||||
});
|
||||
|
||||
return { members, fetchMembers, removeMember, updateMember };
|
||||
return { removeMember, updateMember };
|
||||
});
|
||||
|
||||
34
resources/js/utils/useMembersQuery.ts
Normal file
34
resources/js/utils/useMembersQuery.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { Member } from '@/packages/api/src';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useMembersQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['members'],
|
||||
queryFn: async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) throw new Error('No organization');
|
||||
return api.getMembers({
|
||||
params: { organization: organizationId },
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId(),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const members = computed<Member[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
const invalidateMembers = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
members,
|
||||
invalidateMembers,
|
||||
};
|
||||
}
|
||||
@@ -1,35 +1,13 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { computed, ref } from 'vue';
|
||||
import type {
|
||||
CreateProjectMemberBody,
|
||||
ProjectMember,
|
||||
ProjectMemberResponse,
|
||||
UpdateProjectMemberBody,
|
||||
} from '@/packages/api/src';
|
||||
import type { CreateProjectMemberBody, UpdateProjectMemberBody } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export const useProjectMembersStore = defineStore('project-members', () => {
|
||||
const projectMemberResponse = ref<ProjectMemberResponse | null>(null);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
async function fetchProjectMembers(projectId: string) {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
projectMemberResponse.value = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.getProjectMembers({
|
||||
params: {
|
||||
organization: organization,
|
||||
project: projectId,
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
'Failed to fetch project members'
|
||||
);
|
||||
}
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function createProjectMember(
|
||||
projectId: string,
|
||||
@@ -48,7 +26,7 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
||||
'Project member added successfully',
|
||||
'Failed to add project member'
|
||||
);
|
||||
await fetchProjectMembers(projectId);
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +47,11 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
||||
'Project member updated successfully',
|
||||
'Failed to update project member'
|
||||
);
|
||||
await fetchProjectMembers(response.data.project_id);
|
||||
if (response?.data?.project_id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['projectMembers', response.data.project_id],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +69,11 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
||||
'Project member removed successfully',
|
||||
'Failed to remove project member'
|
||||
);
|
||||
await fetchProjectMembers(projectId);
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||
}
|
||||
}
|
||||
|
||||
const projectMembers = computed<ProjectMember[]>(() => projectMemberResponse.value?.data || []);
|
||||
|
||||
return {
|
||||
projectMembers,
|
||||
fetchProjectMembers,
|
||||
createProjectMember,
|
||||
deleteProjectMember,
|
||||
updateProjectMember,
|
||||
|
||||
39
resources/js/utils/useProjectMembersQuery.ts
Normal file
39
resources/js/utils/useProjectMembersQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { ProjectMember } from '@/packages/api/src';
|
||||
import { computed, type Ref } from 'vue';
|
||||
|
||||
export function useProjectMembersQuery(projectId: Ref<string | null> | string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectIdValue = computed(() => {
|
||||
return typeof projectId === 'string' ? projectId : projectId.value;
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['projectMembers', projectIdValue],
|
||||
queryFn: async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
const pid = projectIdValue.value;
|
||||
if (!organizationId || !pid) throw new Error('No organization or project');
|
||||
return api.getProjectMembers({
|
||||
params: { organization: organizationId, project: pid },
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId() && !!projectIdValue.value,
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const projectMembers = computed<ProjectMember[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
const invalidateProjectMembers = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectIdValue.value] });
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
projectMembers,
|
||||
invalidateProjectMembers,
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export function useProjectsQuery() {
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId(),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const projects = computed<Project[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineStore, storeToRefs } from 'pinia';
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { type Component, computed, ref } from 'vue';
|
||||
import type {
|
||||
@@ -8,11 +8,11 @@ import type {
|
||||
} from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId, getCurrentRole, getCurrentUser } from '@/utils/useUser';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useProjectsStore } from '@/utils/useProjects';
|
||||
import { useMembersStore } from '@/utils/useMembers';
|
||||
import { useTasksStore } from '@/utils/useTasks';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { useTagsStore } from '@/utils/useTags';
|
||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||
import { CheckCircleIcon, UserCircleIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||
import { DocumentTextIcon, FolderIcon } from '@heroicons/vue/16/solid';
|
||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||
@@ -32,6 +32,13 @@ export const useReportingStore = defineStore('reporting', () => {
|
||||
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
// Cache query composables to avoid creating new subscriptions on every call
|
||||
const { projects } = useProjectsQuery();
|
||||
const { members } = useMembersQuery();
|
||||
const { tasks } = useTasksQuery();
|
||||
const { clients } = useClientsQuery();
|
||||
const { tags } = useTagsQuery();
|
||||
|
||||
async function fetchGraphReporting(params: AggregatedTimeEntriesQueryParams) {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
@@ -93,31 +100,21 @@ export const useReportingStore = defineStore('reporting', () => {
|
||||
}
|
||||
|
||||
if (type === 'project') {
|
||||
const projectsStore = useProjectsStore();
|
||||
const { projects } = storeToRefs(projectsStore);
|
||||
return projects.value.find((project) => project.id === key)?.name;
|
||||
}
|
||||
if (type === 'user') {
|
||||
if (getCurrentRole() === 'employee') {
|
||||
return getCurrentUser().name;
|
||||
}
|
||||
const memberStore = useMembersStore();
|
||||
const { members } = storeToRefs(memberStore);
|
||||
return members.value.find((member) => member.user_id === key)?.name;
|
||||
}
|
||||
if (type === 'task') {
|
||||
const taskStore = useTasksStore();
|
||||
const { tasks } = storeToRefs(taskStore);
|
||||
return tasks.value.find((task) => task.id === key)?.name;
|
||||
}
|
||||
if (type === 'client') {
|
||||
const clientsStore = useClientsStore();
|
||||
const { clients } = storeToRefs(clientsStore);
|
||||
return clients.value.find((client) => client.id === key)?.name;
|
||||
}
|
||||
if (type === 'tag') {
|
||||
const tagsStore = useTagsStore();
|
||||
const { tags } = storeToRefs(tagsStore);
|
||||
return tags.value.find((tag) => tag.id === key)?.name;
|
||||
}
|
||||
if (type === 'billable') {
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { Tag } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export const useTagsStore = defineStore('tags', () => {
|
||||
const tags = ref<Tag[]>([]);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
async function fetchTags() {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId) {
|
||||
const response = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.getTags({
|
||||
params: {
|
||||
organization: organizationId,
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
'Failed to fetch tags'
|
||||
);
|
||||
if (response?.data) {
|
||||
tags.value = response.data;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to fetch current tags because organization ID is missing.');
|
||||
}
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function deleteTag(tagId: string) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
@@ -43,11 +23,11 @@ export const useTagsStore = defineStore('tags', () => {
|
||||
'Tag deleted successfully',
|
||||
'Failed to delete tag'
|
||||
);
|
||||
await fetchTags();
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag(name: string) {
|
||||
async function createTag(name: string): Promise<Tag | undefined> {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId) {
|
||||
const response = await handleApiRequestNotifications(
|
||||
@@ -66,7 +46,7 @@ export const useTagsStore = defineStore('tags', () => {
|
||||
'Failed to create tag'
|
||||
);
|
||||
if (response?.data) {
|
||||
tags.value.unshift(response.data);
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
return response.data;
|
||||
}
|
||||
} else {
|
||||
@@ -74,5 +54,5 @@ export const useTagsStore = defineStore('tags', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { tags, fetchTags, createTag, deleteTag };
|
||||
return { createTag, deleteTag };
|
||||
});
|
||||
|
||||
34
resources/js/utils/useTagsQuery.ts
Normal file
34
resources/js/utils/useTagsQuery.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { Tag } from '@/packages/api/src';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useTagsQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['tags'],
|
||||
queryFn: async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) throw new Error('No organization');
|
||||
return api.getTags({
|
||||
params: { organization: organizationId },
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId(),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const tags = computed<Tag[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
const invalidateTags = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
tags,
|
||||
invalidateTags,
|
||||
};
|
||||
}
|
||||
@@ -1,32 +1,13 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { CreateTaskBody, Task, UpdateTaskBody } from '@/packages/api/src';
|
||||
import type { CreateTaskBody, UpdateTaskBody } from '@/packages/api/src';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export const useTasksStore = defineStore('tasks', () => {
|
||||
const tasks = ref<Task[]>(reactive([]));
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
async function fetchTasks() {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId) {
|
||||
const tasksResponse = await handleApiRequestNotifications(() =>
|
||||
api.getTasks({
|
||||
params: {
|
||||
organization: organizationId,
|
||||
},
|
||||
queries: {
|
||||
done: 'all',
|
||||
},
|
||||
})
|
||||
);
|
||||
if (tasksResponse?.data) {
|
||||
tasks.value = tasksResponse.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function updateTask(taskId: string, taskBody: UpdateTaskBody) {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
@@ -42,7 +23,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
||||
'Task updated successfully',
|
||||
'Failed to update task'
|
||||
);
|
||||
await fetchTasks();
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +40,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
||||
'Task created successfully',
|
||||
'Failed to create task'
|
||||
);
|
||||
await fetchTasks();
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +58,11 @@ export const useTasksStore = defineStore('tasks', () => {
|
||||
'Task deleted successfully',
|
||||
'Failed to delete task'
|
||||
);
|
||||
await fetchTasks();
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
fetchTasks,
|
||||
updateTask,
|
||||
createTask,
|
||||
deleteTask,
|
||||
|
||||
35
resources/js/utils/useTasksQuery.ts
Normal file
35
resources/js/utils/useTasksQuery.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { api } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { Task } from '@/packages/api/src';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useTasksQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['tasks'],
|
||||
queryFn: async () => {
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (!organizationId) throw new Error('No organization');
|
||||
return api.getTasks({
|
||||
params: { organization: organizationId },
|
||||
queries: { done: 'all' },
|
||||
});
|
||||
},
|
||||
enabled: () => !!getCurrentOrganizationId(),
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
});
|
||||
|
||||
const tasks = computed<Task[]>(() => query.data.value?.data ?? []);
|
||||
|
||||
const invalidateTasks = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
};
|
||||
|
||||
return {
|
||||
...query,
|
||||
tasks,
|
||||
invalidateTasks,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user