From 556bbedeca4b5a38e83d748a14e8b64b57632a41 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Wed, 18 Feb 2026 22:32:56 +0100 Subject: [PATCH] add dynamic loading of paginated endpoints above page_limit add request classes and fix collection typing for clients, tasks and tags --- .../Api/V1/ProjectMemberController.php | 3 +- .../Controllers/Api/V1/ReportController.php | 3 +- app/Http/Controllers/Api/V1/TagController.php | 3 +- .../V1/Invitation/InvitationIndexRequest.php | 5 + .../Requests/V1/Member/MemberIndexRequest.php | 5 + .../ProjectMemberIndexRequest.php | 27 ++++ .../Requests/V1/Report/ReportIndexRequest.php | 27 ++++ app/Http/Requests/V1/Tag/TagIndexRequest.php | 27 ++++ .../Requests/V1/Task/TaskIndexRequest.php | 5 + .../Resources/V1/Client/ClientCollection.php | 3 +- app/Http/Resources/V1/Tag/TagCollection.php | 3 +- resources/js/Pages/ReportingShared.vue | 121 ++--------------- .../packages/api/src/openapi.json.client.ts | 128 +++++++++++++++++- resources/js/utils/fetchAllPages.ts | 22 +++ resources/js/utils/prefetch.ts | 49 +++---- resources/js/utils/useClientsQuery.ts | 16 ++- resources/js/utils/useInvitations.ts | 32 +++-- resources/js/utils/useMembersQuery.ts | 15 +- resources/js/utils/useProjectMembersQuery.ts | 18 ++- resources/js/utils/useProjectsQuery.ts | 16 ++- resources/js/utils/useReportsQuery.ts | 12 ++ resources/js/utils/useTagsQuery.ts | 15 +- resources/js/utils/useTasksQuery.ts | 16 ++- 23 files changed, 386 insertions(+), 185 deletions(-) create mode 100644 app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php create mode 100644 app/Http/Requests/V1/Report/ReportIndexRequest.php create mode 100644 app/Http/Requests/V1/Tag/TagIndexRequest.php create mode 100644 resources/js/utils/fetchAllPages.ts create mode 100644 resources/js/utils/useReportsQuery.ts diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php index ff6c03c2..6e981827 100644 --- a/app/Http/Controllers/Api/V1/ProjectMemberController.php +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -6,6 +6,7 @@ namespace App\Http\Controllers\Api\V1; use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException; use App\Exceptions\Api\UserIsAlreadyMemberOfProjectApiException; +use App\Http\Requests\V1\ProjectMember\ProjectMemberIndexRequest; use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest; use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest; use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection; @@ -41,7 +42,7 @@ class ProjectMemberController extends Controller * * @operationId getProjectMembers */ - public function index(Organization $organization, Project $project): ProjectMemberCollection + public function index(Organization $organization, Project $project, ProjectMemberIndexRequest $request): ProjectMemberCollection { $this->checkPermission($organization, 'project-members:view', $project); diff --git a/app/Http/Controllers/Api/V1/ReportController.php b/app/Http/Controllers/Api/V1/ReportController.php index 5a8ca312..1f89fa01 100644 --- a/app/Http/Controllers/Api/V1/ReportController.php +++ b/app/Http/Controllers/Api/V1/ReportController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; use App\Enums\Weekday; +use App\Http\Requests\V1\Report\ReportIndexRequest; use App\Http\Requests\V1\Report\ReportStoreRequest; use App\Http\Requests\V1\Report\ReportUpdateRequest; use App\Http\Resources\V1\Report\DetailedReportResource; @@ -40,7 +41,7 @@ class ReportController extends Controller * * @operationId getReports */ - public function index(Organization $organization): ReportCollection + public function index(Organization $organization, ReportIndexRequest $request): ReportCollection { $this->checkPermission($organization, 'reports:view'); diff --git a/app/Http/Controllers/Api/V1/TagController.php b/app/Http/Controllers/Api/V1/TagController.php index 22b6d0a2..2dd17424 100644 --- a/app/Http/Controllers/Api/V1/TagController.php +++ b/app/Http/Controllers/Api/V1/TagController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; use App\Exceptions\Api\EntityStillInUseApiException; +use App\Http\Requests\V1\Tag\TagIndexRequest; use App\Http\Requests\V1\Tag\TagStoreRequest; use App\Http\Requests\V1\Tag\TagUpdateRequest; use App\Http\Resources\V1\Tag\TagCollection; @@ -34,7 +35,7 @@ class TagController extends Controller * * @throws AuthorizationException */ - public function index(Organization $organization): TagCollection + public function index(Organization $organization, TagIndexRequest $request): TagCollection { $this->checkPermission($organization, 'tags:view'); diff --git a/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php b/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php index 5c013c4e..94d896eb 100644 --- a/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php +++ b/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php @@ -21,6 +21,11 @@ class InvitationIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], ]; } } diff --git a/app/Http/Requests/V1/Member/MemberIndexRequest.php b/app/Http/Requests/V1/Member/MemberIndexRequest.php index 706dc3c7..b3d350aa 100644 --- a/app/Http/Requests/V1/Member/MemberIndexRequest.php +++ b/app/Http/Requests/V1/Member/MemberIndexRequest.php @@ -21,6 +21,11 @@ class MemberIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], ]; } } diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php new file mode 100644 index 00000000..169933c0 --- /dev/null +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Report/ReportIndexRequest.php b/app/Http/Requests/V1/Report/ReportIndexRequest.php new file mode 100644 index 00000000..cde3289e --- /dev/null +++ b/app/Http/Requests/V1/Report/ReportIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Tag/TagIndexRequest.php b/app/Http/Requests/V1/Tag/TagIndexRequest.php new file mode 100644 index 00000000..e1f706b9 --- /dev/null +++ b/app/Http/Requests/V1/Tag/TagIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Task/TaskIndexRequest.php b/app/Http/Requests/V1/Task/TaskIndexRequest.php index 6fd80307..e9305b09 100644 --- a/app/Http/Requests/V1/Task/TaskIndexRequest.php +++ b/app/Http/Requests/V1/Task/TaskIndexRequest.php @@ -26,6 +26,11 @@ class TaskIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], 'project_id' => [ ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ diff --git a/app/Http/Resources/V1/Client/ClientCollection.php b/app/Http/Resources/V1/Client/ClientCollection.php index 9c5de3cb..24e57bbd 100644 --- a/app/Http/Resources/V1/Client/ClientCollection.php +++ b/app/Http/Resources/V1/Client/ClientCollection.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace App\Http\Resources\V1\Client; +use App\Http\Resources\PaginatedResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection; -class ClientCollection extends ResourceCollection +class ClientCollection extends ResourceCollection implements PaginatedResourceCollection { /** * The resource that this resource collects. diff --git a/app/Http/Resources/V1/Tag/TagCollection.php b/app/Http/Resources/V1/Tag/TagCollection.php index 67c33977..707b9fa8 100644 --- a/app/Http/Resources/V1/Tag/TagCollection.php +++ b/app/Http/Resources/V1/Tag/TagCollection.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace App\Http\Resources\V1\Tag; +use App\Http\Resources\PaginatedResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection; -class TagCollection extends ResourceCollection +class TagCollection extends ResourceCollection implements PaginatedResourceCollection { /** * The resource that this resource collects. diff --git a/resources/js/Pages/ReportingShared.vue b/resources/js/Pages/ReportingShared.vue index 3e9c94b5..504a7c9f 100644 --- a/resources/js/Pages/ReportingShared.vue +++ b/resources/js/Pages/ReportingShared.vue @@ -2,29 +2,10 @@ import MainContainer from '@/packages/ui/src/MainContainer.vue'; import AppLayout from '@/Layouts/AppLayout.vue'; import PageTitle from '@/Components/Common/PageTitle.vue'; -import { - ChartBarIcon, - ChevronLeftIcon, - ChevronDoubleLeftIcon, - ChevronRightIcon, - ChevronDoubleRightIcon, - CreditCardIcon, - UserGroupIcon, -} from '@heroicons/vue/20/solid'; -import { computed, ref, watch } from 'vue'; +import { ChartBarIcon, CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid'; +import { computed } from 'vue'; -import { api, type ReportIndexResponse } from '@/packages/api/src'; -import { - PaginationEllipsis, - PaginationFirst, - PaginationLast, - PaginationList, - PaginationListItem, - PaginationNext, - PaginationPrev, - PaginationRoot, -} from 'radix-vue'; -import { useQuery, useQueryClient } from '@tanstack/vue-query'; +import { useQuery } from '@tanstack/vue-query'; import { getCurrentOrganizationId } from '@/utils/useUser'; import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue'; import ReportTable from '@/Components/Common/Report/ReportTable.vue'; @@ -32,37 +13,22 @@ import { isAllowedToPerformPremiumAction, isBillingActivated } from '@/utils/bil import { canManageBilling, canUpdateOrganization } from '@/utils/permissions'; import PrimaryButton from '../packages/ui/src/Buttons/PrimaryButton.vue'; import { Link } from '@inertiajs/vue3'; +import { fetchAllReports } from '@/utils/useReportsQuery'; -const pageLimit = 15; -const currentPage = ref(1); - -const { data: reportsResponse } = useQuery({ - queryKey: computed(() => ['reports', getCurrentOrganizationId(), currentPage.value]), +const { data: reportsData } = useQuery({ + queryKey: computed(() => ['reports', getCurrentOrganizationId()]), enabled: !!getCurrentOrganizationId(), - queryFn: () => - api.getReports({ - params: { - organization: getCurrentOrganizationId() || '', - }, - }), + queryFn: async () => { + const organizationId = getCurrentOrganizationId(); + if (!organizationId) throw new Error('No organization'); + const data = await fetchAllReports(organizationId); + return { data }; + }, + staleTime: 1000 * 30, }); const reports = computed(() => { - return reportsResponse.value?.data ?? []; -}); - -const totalPages = computed(() => { - return 1; -}); - -const queryClient = useQueryClient(); -async function updateFilteredTimeEntries() { - await queryClient.invalidateQueries({ - queryKey: ['reports'], - }); -} -watch(currentPage, () => { - updateFilteredTimeEntries(); + return reportsData.value?.data ?? []; }); @@ -110,64 +76,5 @@ watch(currentPage, () => { - - - -
- - - - - - - -
- -
- - - - - - -
-
-
- diff --git a/resources/js/packages/api/src/openapi.json.client.ts b/resources/js/packages/api/src/openapi.json.client.ts index ae8c288c..74a7ed21 100644 --- a/resources/js/packages/api/src/openapi.json.client.ts +++ b/resources/js/packages/api/src/openapi.json.client.ts @@ -33,7 +33,6 @@ const ClientResource = z updated_at: z.string(), }) .passthrough(); -const ClientCollection = z.array(ClientResource); const ClientStoreRequest = z.object({ name: z.string().min(1).max(255) }).passthrough(); const ClientUpdateRequest = z .object({ name: z.string().min(1).max(255), is_archived: z.boolean().optional() }) @@ -598,7 +597,6 @@ const DetailedWithDataReportResource = z const TagResource = z .object({ id: z.string(), name: z.string(), created_at: z.string(), updated_at: z.string() }) .passthrough(); -const TagCollection = z.array(TagResource); const TagStoreRequest = z.object({ name: z.string().min(1).max(255) }).passthrough(); const TagUpdateRequest = z.object({ name: z.string().min(1).max(255) }).passthrough(); const TaskResource = z @@ -711,7 +709,6 @@ export const schemas = { ApiTokenStoreRequest, ApiTokenWithAccessTokenResource, ClientResource, - ClientCollection, ClientStoreRequest, ClientUpdateRequest, ImportRequest, @@ -755,7 +752,6 @@ export const schemas = { ReportUpdateRequest, DetailedWithDataReportResource, TagResource, - TagCollection, TagStoreRequest, TagUpdateRequest, TaskResource, @@ -1201,7 +1197,39 @@ const endpoints = makeApi([ schema: z.enum(['true', 'false', 'all']).optional(), }, ], - response: z.object({ data: ClientCollection }).passthrough(), + response: z + .object({ + data: z.array(ClientResource), + links: z + .object({ + first: z.union([z.string(), z.null()]), + last: z.union([z.string(), z.null()]), + prev: z.union([z.string(), z.null()]), + next: z.union([z.string(), z.null()]), + }) + .passthrough(), + meta: z + .object({ + current_page: z.number().int(), + from: z.union([z.number(), z.null()]), + last_page: z.number().int(), + links: z.array( + z + .object({ + url: z.union([z.string(), z.null()]), + label: z.string(), + active: z.boolean(), + }) + .passthrough() + ), + path: z.union([z.string(), z.null()]), + per_page: z.number().int(), + to: z.union([z.number(), z.null()]), + total: z.number().int(), + }) + .passthrough(), + }) + .passthrough(), errors: [ { status: 401, @@ -1512,6 +1540,11 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, ], response: z .object({ @@ -2137,6 +2170,11 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, ], response: z .object({ @@ -2742,6 +2780,11 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, ], response: z .object({ @@ -2792,6 +2835,13 @@ const endpoints = makeApi([ description: `Not found`, schema: z.object({ message: z.string() }).passthrough(), }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ message: z.string(), errors: z.record(z.array(z.string())) }) + .passthrough(), + }, ], }, { @@ -2860,6 +2910,11 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, ], response: z .object({ @@ -2910,6 +2965,13 @@ const endpoints = makeApi([ description: `Not found`, schema: z.object({ message: z.string() }).passthrough(), }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ message: z.string(), errors: z.record(z.array(z.string())) }) + .passthrough(), + }, ], }, { @@ -3086,8 +3148,45 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, ], - response: z.object({ data: TagCollection }).passthrough(), + response: z + .object({ + data: z.array(TagResource), + links: z + .object({ + first: z.union([z.string(), z.null()]), + last: z.union([z.string(), z.null()]), + prev: z.union([z.string(), z.null()]), + next: z.union([z.string(), z.null()]), + }) + .passthrough(), + meta: z + .object({ + current_page: z.number().int(), + from: z.union([z.number(), z.null()]), + last_page: z.number().int(), + links: z.array( + z + .object({ + url: z.union([z.string(), z.null()]), + label: z.string(), + active: z.boolean(), + }) + .passthrough() + ), + path: z.union([z.string(), z.null()]), + per_page: z.number().int(), + to: z.union([z.number(), z.null()]), + total: z.number().int(), + }) + .passthrough(), + }) + .passthrough(), errors: [ { status: 401, @@ -3104,6 +3203,13 @@ const endpoints = makeApi([ description: `Not found`, schema: z.object({ message: z.string() }).passthrough(), }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ message: z.string(), errors: z.record(z.array(z.string())) }) + .passthrough(), + }, ], }, { @@ -3251,6 +3357,11 @@ const endpoints = makeApi([ type: 'Path', schema: z.string(), }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).lte(2147483647).optional(), + }, { name: 'project_id', type: 'Query', @@ -4230,6 +4341,11 @@ If the group parameters are all set to `null` or are all missing, the type: 'Query', schema: z.array(z.string().uuid()).min(1).optional(), }, + { + name: 'client_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, { name: 'project_ids', type: 'Query', diff --git a/resources/js/utils/fetchAllPages.ts b/resources/js/utils/fetchAllPages.ts new file mode 100644 index 00000000..34bb3fe1 --- /dev/null +++ b/resources/js/utils/fetchAllPages.ts @@ -0,0 +1,22 @@ +/** + * Fetches all pages from a paginated Laravel API endpoint. + * Uses `meta.last_page` to determine the total number of pages, + * so only a single request is made when all data fits on one page. + */ +export async function fetchAllPages( + fetchPage: (page: number) => Promise<{ + data: T[]; + meta: { per_page: number; last_page: number }; + }> +): Promise { + const firstResponse = await fetchPage(1); + const allItems: T[] = [...firstResponse.data]; + const { last_page } = firstResponse.meta; + + for (let page = 2; page <= last_page; page++) { + const response = await fetchPage(page); + allItems.push(...response.data); + } + + return allItems; +} diff --git a/resources/js/utils/prefetch.ts b/resources/js/utils/prefetch.ts index 945d6222..e3168377 100644 --- a/resources/js/utils/prefetch.ts +++ b/resources/js/utils/prefetch.ts @@ -8,6 +8,13 @@ import { createCalendarQueryKey, fetchAllCalendarEntries, } from '@/utils/useTimeEntriesCalendarQuery'; +import { fetchAllProjects } from '@/utils/useProjectsQuery'; +import { fetchAllTasks } from '@/utils/useTasksQuery'; +import { fetchAllTags } from '@/utils/useTagsQuery'; +import { fetchAllClients } from '@/utils/useClientsQuery'; +import { fetchAllMembers } from '@/utils/useMembersQuery'; +import { fetchAllReports } from '@/utils/useReportsQuery'; +import { fetchAllProjectMembers } from '@/utils/useProjectMembersQuery'; /** * Route patterns mapped to their prefetch functions. @@ -152,12 +159,8 @@ function prefetchProjects(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['projects', organizationId], - queryFn: () => - api.getProjects({ - params: { organization: organizationId }, - queries: { archived: 'all' }, - }), - staleTime: 30000, // Consider fresh for 30 seconds + queryFn: async () => ({ data: await fetchAllProjects(organizationId) }), + staleTime: 30000, }); } @@ -167,11 +170,7 @@ function prefetchTasks(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['tasks', organizationId], - queryFn: () => - api.getTasks({ - params: { organization: organizationId }, - queries: { done: 'all' }, - }), + queryFn: async () => ({ data: await fetchAllTasks(organizationId) }), staleTime: 30000, }); } @@ -182,10 +181,7 @@ function prefetchTags(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['tags', organizationId], - queryFn: () => - api.getTags({ - params: { organization: organizationId }, - }), + queryFn: async () => ({ data: await fetchAllTags(organizationId) }), staleTime: 30000, }); } @@ -196,11 +192,7 @@ function prefetchClients(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['clients', organizationId], - queryFn: () => - api.getClients({ - params: { organization: organizationId }, - queries: { archived: 'all' }, - }), + queryFn: async () => ({ data: await fetchAllClients(organizationId) }), staleTime: 30000, }); } @@ -211,10 +203,7 @@ function prefetchMembers(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['members', organizationId], - queryFn: () => - api.getMembers({ - params: { organization: organizationId }, - }), + queryFn: async () => ({ data: await fetchAllMembers(organizationId) }), staleTime: 30000, }); } @@ -225,10 +214,7 @@ function prefetchReports(queryClient: QueryClient) { queryClient.prefetchQuery({ queryKey: ['reports', organizationId], - queryFn: () => - api.getReports({ - params: { organization: organizationId }, - }), + queryFn: async () => ({ data: await fetchAllReports(organizationId) }), staleTime: 30000, }); } @@ -277,10 +263,9 @@ function prefetchProjectMembers(queryClient: QueryClient, projectId: string) { queryClient.prefetchQuery({ queryKey: ['projectMembers', organizationId, projectId], - queryFn: () => - api.getProjectMembers({ - params: { organization: organizationId, project: projectId }, - }), + queryFn: async () => ({ + data: await fetchAllProjectMembers(organizationId, projectId), + }), staleTime: 30000, }); } diff --git a/resources/js/utils/useClientsQuery.ts b/resources/js/utils/useClientsQuery.ts index 81050413..fa06c684 100644 --- a/resources/js/utils/useClientsQuery.ts +++ b/resources/js/utils/useClientsQuery.ts @@ -3,6 +3,16 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { Client } from '@/packages/api/src'; import { computed } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllClients(organizationId: string): Promise { + return fetchAllPages((page) => + api.getClients({ + params: { organization: organizationId }, + queries: { archived: 'all', page }, + }) + ); +} export function useClientsQuery() { const queryClient = useQueryClient(); @@ -12,10 +22,8 @@ export function useClientsQuery() { queryFn: async () => { const organizationId = getCurrentOrganizationId(); if (!organizationId) throw new Error('No organization'); - return api.getClients({ - params: { organization: organizationId }, - queries: { archived: 'all' }, - }); + const data = await fetchAllClients(organizationId); + return { data }; }, enabled: () => !!getCurrentOrganizationId(), staleTime: 1000 * 30, // 30 seconds diff --git a/resources/js/utils/useInvitations.ts b/resources/js/utils/useInvitations.ts index bdaab928..e2aa32be 100644 --- a/resources/js/utils/useInvitations.ts +++ b/resources/js/utils/useInvitations.ts @@ -1,31 +1,35 @@ import { defineStore } from 'pinia'; import { api } from '@/packages/api/src'; import { computed, ref } from 'vue'; -import type { - InvitationsIndexResponse, - CreateInvitationBody, - Invitation, -} from '@/packages/api/src'; +import type { CreateInvitationBody, Invitation } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import { useNotificationsStore } from '@/utils/notification'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllInvitations(organizationId: string): Promise { + return fetchAllPages((page) => + api.getInvitations({ + params: { organization: organizationId }, + queries: { page }, + }) + ); +} export const useInvitationsStore = defineStore('invitations', () => { - const invitationsResponse = ref(null); + const invitationsData = ref([]); const { handleApiRequestNotifications } = useNotificationsStore(); async function fetchInvitations() { const organization = getCurrentOrganizationId(); if (organization) { - invitationsResponse.value = await handleApiRequestNotifications( - () => - api.getInvitations({ - params: { - organization: organization, - }, - }), + const data = await handleApiRequestNotifications( + () => fetchAllInvitations(organization), undefined, 'Failed to fetch invitations' ); + if (data) { + invitationsData.value = data; + } } } @@ -47,7 +51,7 @@ export const useInvitationsStore = defineStore('invitations', () => { } const invitations = computed(() => { - return invitationsResponse.value?.data || []; + return invitationsData.value; }); return { invitations, fetchInvitations, createInvitation }; diff --git a/resources/js/utils/useMembersQuery.ts b/resources/js/utils/useMembersQuery.ts index e37500aa..25f85fff 100644 --- a/resources/js/utils/useMembersQuery.ts +++ b/resources/js/utils/useMembersQuery.ts @@ -3,6 +3,16 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { Member } from '@/packages/api/src'; import { computed } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllMembers(organizationId: string): Promise { + return fetchAllPages((page) => + api.getMembers({ + params: { organization: organizationId }, + queries: { page }, + }) + ); +} export function useMembersQuery() { const queryClient = useQueryClient(); @@ -12,9 +22,8 @@ export function useMembersQuery() { queryFn: async () => { const organizationId = getCurrentOrganizationId(); if (!organizationId) throw new Error('No organization'); - return api.getMembers({ - params: { organization: organizationId }, - }); + const data = await fetchAllMembers(organizationId); + return { data }; }, enabled: () => !!getCurrentOrganizationId(), staleTime: 1000 * 30, // 30 seconds diff --git a/resources/js/utils/useProjectMembersQuery.ts b/resources/js/utils/useProjectMembersQuery.ts index 34872daa..c1e1aa4d 100644 --- a/resources/js/utils/useProjectMembersQuery.ts +++ b/resources/js/utils/useProjectMembersQuery.ts @@ -3,6 +3,19 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { ProjectMember } from '@/packages/api/src'; import { computed, type Ref } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllProjectMembers( + organizationId: string, + projectId: string +): Promise { + return fetchAllPages((page) => + api.getProjectMembers({ + params: { organization: organizationId, project: projectId }, + queries: { page }, + }) + ); +} export function useProjectMembersQuery(projectId: Ref | string) { const queryClient = useQueryClient(); @@ -21,9 +34,8 @@ export function useProjectMembersQuery(projectId: Ref | string) { 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 }, - }); + const data = await fetchAllProjectMembers(organizationId, pid); + return { data }; }, enabled: () => !!getCurrentOrganizationId() && !!projectIdValue.value, staleTime: 1000 * 30, // 30 seconds diff --git a/resources/js/utils/useProjectsQuery.ts b/resources/js/utils/useProjectsQuery.ts index a0e6a7b2..cbc4a21d 100644 --- a/resources/js/utils/useProjectsQuery.ts +++ b/resources/js/utils/useProjectsQuery.ts @@ -3,6 +3,16 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { Project } from '@/packages/api/src'; import { computed } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllProjects(organizationId: string): Promise { + return fetchAllPages((page) => + api.getProjects({ + params: { organization: organizationId }, + queries: { archived: 'all', page }, + }) + ); +} export function useProjectsQuery() { const queryClient = useQueryClient(); @@ -12,10 +22,8 @@ export function useProjectsQuery() { queryFn: async () => { const organizationId = getCurrentOrganizationId(); if (!organizationId) throw new Error('No organization'); - return api.getProjects({ - params: { organization: organizationId }, - queries: { archived: 'all' }, - }); + const data = await fetchAllProjects(organizationId); + return { data }; }, enabled: () => !!getCurrentOrganizationId(), staleTime: 1000 * 30, // 30 seconds diff --git a/resources/js/utils/useReportsQuery.ts b/resources/js/utils/useReportsQuery.ts new file mode 100644 index 00000000..00fdac7b --- /dev/null +++ b/resources/js/utils/useReportsQuery.ts @@ -0,0 +1,12 @@ +import { api } from '@/packages/api/src'; +import type { Report } from '@/packages/api/src'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllReports(organizationId: string): Promise { + return fetchAllPages((page) => + api.getReports({ + params: { organization: organizationId }, + queries: { page }, + }) + ); +} diff --git a/resources/js/utils/useTagsQuery.ts b/resources/js/utils/useTagsQuery.ts index 3ba6a66e..d0555488 100644 --- a/resources/js/utils/useTagsQuery.ts +++ b/resources/js/utils/useTagsQuery.ts @@ -3,6 +3,16 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { Tag } from '@/packages/api/src'; import { computed } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllTags(organizationId: string): Promise { + return fetchAllPages((page) => + api.getTags({ + params: { organization: organizationId }, + queries: { page }, + }) + ); +} export function useTagsQuery() { const queryClient = useQueryClient(); @@ -12,9 +22,8 @@ export function useTagsQuery() { queryFn: async () => { const organizationId = getCurrentOrganizationId(); if (!organizationId) throw new Error('No organization'); - return api.getTags({ - params: { organization: organizationId }, - }); + const data = await fetchAllTags(organizationId); + return { data }; }, enabled: () => !!getCurrentOrganizationId(), staleTime: 1000 * 30, // 30 seconds diff --git a/resources/js/utils/useTasksQuery.ts b/resources/js/utils/useTasksQuery.ts index 93ee83fd..69e02c62 100644 --- a/resources/js/utils/useTasksQuery.ts +++ b/resources/js/utils/useTasksQuery.ts @@ -3,6 +3,16 @@ import { api } from '@/packages/api/src'; import { getCurrentOrganizationId } from '@/utils/useUser'; import type { Task } from '@/packages/api/src'; import { computed } from 'vue'; +import { fetchAllPages } from '@/utils/fetchAllPages'; + +export async function fetchAllTasks(organizationId: string): Promise { + return fetchAllPages((page) => + api.getTasks({ + params: { organization: organizationId }, + queries: { done: 'all', page }, + }) + ); +} export function useTasksQuery() { const queryClient = useQueryClient(); @@ -12,10 +22,8 @@ export function useTasksQuery() { queryFn: async () => { const organizationId = getCurrentOrganizationId(); if (!organizationId) throw new Error('No organization'); - return api.getTasks({ - params: { organization: organizationId }, - queries: { done: 'all' }, - }); + const data = await fetchAllTasks(organizationId); + return { data }; }, enabled: () => !!getCurrentOrganizationId(), staleTime: 1000 * 30, // 30 seconds