diff --git a/src/hooks/useInfiniteScroller.ts b/src/hooks/useInfiniteScroller.ts index 804674e7a..1aaba5a80 100644 --- a/src/hooks/useInfiniteScroller.ts +++ b/src/hooks/useInfiniteScroller.ts @@ -1,19 +1,6 @@ -import { - type ApiEndpointQuery, - type BaseQueryFn, - type EndpointDefinitions, - type FetchArgs, - type FetchBaseQueryError, - type QueryDefinition, - QueryStatus, - skipToken, -} from '@reduxjs/toolkit/query' -import {useEffect, useState} from 'react' -import type {ApiSlug} from '@/environment' -import type {Paginated, PaginationQueryArgs} from '@/types/api' -import type {SerializedError} from '@reduxjs/toolkit' -import {useSelector} from '@/hooks/redux/useSelector' -import {baseApi} from '@/services/baseApi' +import {skipToken} from '@reduxjs/toolkit/query' +import {useEffect} from 'react' +import type {ApiEndpointInfinite, PaginationQueryArgs} from '@/types/api' const getEmptyItems = ( length: number, @@ -22,11 +9,12 @@ const getEmptyItems = ( keyName: keyof DummyItem, ) => length > 0 - ? Array(length) + ? new Array(Math.max(0, length)) .fill(defaultEmptyItem) .map((el, index) => ({ ...el, [keyName]: `dummy-${index + baseIndex}`, + dummy: true, })) : [] @@ -36,127 +24,106 @@ const config = { pageSize: 10, } -type QueryDef = QueryDefinition< - QueryArgs, - BaseQueryFn, - string, - Paginated -> - -type UseQueryHook = (arg: QueryArgs | typeof skipToken) => { - data?: Result - error?: FetchBaseQueryError | SerializedError - isError: boolean - isLoading: boolean -} - +/** + * Builds a paged list for infinite scrolling by combining fetched items with + * dummy placeholder items up to the reported total result size. + * + * The hook automatically fetches the next page once the requested `page` + * reaches the last loaded page. + * + * @param defaultEmptyItem Base item shape used to generate placeholder rows. + * @param endpoint RTK Query infinite endpoint that returns paginated results. + * @param keyName Unique item key used to assign stable ids to dummy rows. + * @param page Current page that should be available in the local result. + * @param pageSize Number of items expected per page. + * @param queryParams Query parameters for the endpoint, or `skipToken` to disable fetching. + * + * @example + * const result = useInfiniteScroller( + * dummyBoatChargingHistoryItem, + * boatChargingApi.endpoints[BoatChargingEndpointName.boatChargingSessions], + * 'id', + * page, + * PAGE_SIZE, + * isLoggedIn + * ? { + * page_size: PAGE_SIZE, + * status: SessionStatus.COMPLETED, + * } + * : skipToken, + * ) + */ export const useInfiniteScroller = < Item, - ItemOrDummyItem, QueryArgs extends PaginationQueryArgs, >( - defaultEmptyItem: ItemOrDummyItem, - endpoint: ApiEndpointQuery, EndpointDefinitions>, - keyName: keyof ItemOrDummyItem, - useQueryHook: UseQueryHook>, + defaultEmptyItem: Item & {dummy?: boolean}, + endpoint: ApiEndpointInfinite, + keyName: keyof (Item & {dummy?: boolean}), page = config.page, pageSize = config.pageSize, queryParams: QueryArgs | typeof skipToken = {} as QueryArgs, ) => { - const reduxApiState = useSelector(state => state[baseApi.reducerPath]) - const [totalPages, setTotalPages] = useState(config.totalPages) - - const { - data: previousData, - isError: isErrorPreviousPage, - isLoading: isLoadingPreviousPage, - error: errorPreviousPage, - } = useQueryHook( - page > 1 && queryParams !== skipToken - ? { - ...queryParams, - page: page - 1, - } - : skipToken, - ) - const { data: currentData, isError: isErrorCurrentPage, isLoading: isLoadingCurrentPage, error: errorCurrentPage, - } = useQueryHook( - page <= totalPages && queryParams !== skipToken - ? { - ...queryParams, - page, - } - : skipToken, - ) - - const { - data: nextData, - isError: isErrorNextPage, - isLoading: isLoadingNextPage, - error: errorNextPage, - } = useQueryHook( - page < totalPages && queryParams !== skipToken - ? { - ...queryParams, - page: page + 1, - } - : skipToken, - ) + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = endpoint.useInfiniteQuery(queryParams, {initialPageParam: 1}) useEffect(() => { - if (currentData?.page.totalPages) { - setTotalPages(currentData?.page.totalPages) + if ( + hasNextPage && + page >= (currentData?.pageParams.at(-1) ?? 0) && + !isFetchingNextPage + ) { + void fetchNextPage() } - }, [currentData?.page.totalPages]) + }, [ + currentData?.pageParams, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + page, + ]) - const totalElements = - previousData?.page.totalElements ?? - currentData?.page.totalElements ?? - nextData?.page.totalElements ?? - 0 + const totalElements = currentData?.pages[0]?.page.totalElements ?? 0 - const endpointSelectorState = { - [baseApi.reducerPath]: reduxApiState, - } as Parameters>[0] + const fetchedData = + queryParams === skipToken + ? [] + : (currentData?.pages.flatMap(({result}, index) => + result.map(item => ({...item, page: index + 1})), + ) ?? []) - return { - // create an array of pages with data - data: - queryParams === skipToken - ? [] - : (Array(totalPages) - // fill the array with empty values - .fill({}) - // map over the array and fill it with data - .reduce((acc, _s, index) => { - const {data, status} = endpoint.select({ - ...queryParams, - page: index + 1, - })(endpointSelectorState) - // if there is no data, fill the page with empty items - const pageData = - data?.result && status === QueryStatus.fulfilled - ? data?.result - : getEmptyItems( - Math.min(pageSize, totalElements - index * pageSize), - index * pageSize, - defaultEmptyItem, - keyName, - ) + const numberOfDummyItems = totalElements - fetchedData.length - return [ - ...acc, - ...pageData.map(item => ({...item, page: index + 1})), - ] - }, []) as Array), - error: errorPreviousPage || errorCurrentPage || errorNextPage, - isError: isErrorPreviousPage || isErrorCurrentPage || isErrorNextPage, - isLoading: - isLoadingPreviousPage || isLoadingCurrentPage || isLoadingNextPage, + const data: Array = + queryParams === skipToken + ? [] + : [ + ...fetchedData, + ...getEmptyItems( + numberOfDummyItems, + fetchedData.length, + defaultEmptyItem, + keyName, + ).map((item, index) => ({ + ...item, + page: + Math.floor( + (totalElements - index - fetchedData.length) / pageSize, + ) + 1, + })), + ] + + return { + data, + error: errorCurrentPage, + isError: isErrorCurrentPage, + isLoading: isLoadingCurrentPage, } } diff --git a/src/modules/boat-charging/components/BoatChargingDetails.tsx b/src/modules/boat-charging/components/BoatChargingDetails.tsx index 925cf0965..3e4e74b7c 100644 --- a/src/modules/boat-charging/components/BoatChargingDetails.tsx +++ b/src/modules/boat-charging/components/BoatChargingDetails.tsx @@ -7,7 +7,6 @@ import {Column} from '@/components/ui/layout/Column' import {Paragraph} from '@/components/ui/text/Paragraph' import {Phrase} from '@/components/ui/text/Phrase' import {Title} from '@/components/ui/text/Title' -import {useInterval} from '@/hooks/useInterval' import {getAddressLine1} from '@/modules/address/utils/addDerivedAddressFields' import {BoatChargingDetailsInfoRows} from '@/modules/boat-charging/components/BoatChargingDetailsInfoRows' import {BoatChargingDetailsSocketRadioGroup} from '@/modules/boat-charging/components/BoatChargingDetailsSocketRadioGroup' @@ -31,9 +30,10 @@ export const BoatChargingDetails = ({id}: {id: BoatChargingLocation['id']}) => { data: location, isLoading: isLoadingLocation, isError: isErrorLocation, - refetch: refetchLocationDetails, fulfilledTimeStamp, - } = useBoatChargingLocationDetailsQuery(id ?? skipToken) + } = useBoatChargingLocationDetailsQuery(id ?? skipToken, { + pollingInterval: REFETCH_INTERVAL, + }) const { activeSessions, @@ -41,8 +41,6 @@ export const BoatChargingDetails = ({id}: {id: BoatChargingLocation['id']}) => { isError: isErrorSessions, } = useBoatChargingSessions() - useInterval(refetchLocationDetails, REFETCH_INTERVAL) - const infoRows = useMemo( () => Object.entries({ diff --git a/src/modules/boat-charging/components/history/BoatChargingHistory.tsx b/src/modules/boat-charging/components/history/BoatChargingHistory.tsx index c5c609224..679d19046 100644 --- a/src/modules/boat-charging/components/history/BoatChargingHistory.tsx +++ b/src/modules/boat-charging/components/history/BoatChargingHistory.tsx @@ -11,15 +11,13 @@ import {BoatChargingHistoryEmpty} from '@/modules/boat-charging/components/histo import {BoatChargingHistoryItem} from '@/modules/boat-charging/components/history/BoatChargingHistoryItem' import {BoatChargingHistoryLogin} from '@/modules/boat-charging/components/history/BoatChargingHistoryLogin' import {useIsLoggedIn} from '@/modules/boat-charging/hooks/useIsLoggedIn' -import { - boatChargingApi, - useBoatChargingSessionsQuery, -} from '@/modules/boat-charging/service' +import {boatChargingApi} from '@/modules/boat-charging/service' import { BoatChargingEndpointName, NRGStatus, type BoatChargingSession, SessionStatus, + type BoatChargingLocation, } from '@/modules/boat-charging/types' import {layoutStyles} from '@/styles/layoutStyles' import {getCurrentPage} from '@/utils/pagination/getCurrentPage' @@ -30,24 +28,11 @@ import { const PAGE_SIZE = 20 -type BoatChargingHistoryInfiniteSession = BoatChargingSession & { - dummy?: never +export type BoatChargingHistoryInfiniteItem = BoatChargingSession & { + dummy?: boolean page: number } -type BoatChargingHistoryInfiniteDummySession = Omit< - BoatChargingSession, - 'location' | 'email' -> & { - dummy: true - location: {name: string} - page: number -} - -type BoatChargingHistoryInfiniteItem = - | BoatChargingHistoryInfiniteSession - | BoatChargingHistoryInfiniteDummySession - type BoatChargingHistoryInfiniteSection = { data: BoatChargingHistoryInfiniteItem[] title: string @@ -66,11 +51,10 @@ const isCompletedOrDummySession = ( const dummyBoatChargingHistoryItem: BoatChargingHistoryInfiniteItem = { created_date_time: '1970-01-01T00:00:00Z', currency: 'EUR', - dummy: true, end_date_time: '1970-01-01T00:00:00Z', id: '', kwh: 0, - location: {name: ''}, + location: {name: ''} as BoatChargingLocation, nrg_status: NRGStatus.Created, page: 0, socket_number: '', @@ -78,6 +62,7 @@ const dummyBoatChargingHistoryItem: BoatChargingHistoryInfiniteItem = { station_id: '', status: SessionStatus.COMPLETED, total_cost: 0, + email: '', } export const BoatChargingHistory = () => { @@ -89,7 +74,6 @@ export const BoatChargingHistory = () => { dummyBoatChargingHistoryItem, boatChargingApi.endpoints[BoatChargingEndpointName.boatChargingSessions], 'id', - useBoatChargingSessionsQuery, page, PAGE_SIZE, isLoggedIn diff --git a/src/modules/boat-charging/components/history/BoatChargingHistoryItem.tsx b/src/modules/boat-charging/components/history/BoatChargingHistoryItem.tsx index 0e860141b..df3d75a77 100644 --- a/src/modules/boat-charging/components/history/BoatChargingHistoryItem.tsx +++ b/src/modules/boat-charging/components/history/BoatChargingHistoryItem.tsx @@ -1,24 +1,23 @@ import {memo} from 'react' -import type {BoatChargingSession} from '@/modules/boat-charging/types' +import type {BoatChargingHistoryInfiniteItem} from '@/modules/boat-charging/components/history/BoatChargingHistory' import {NavigationButton} from '@/components/ui/buttons/NavigationButton' import {Box} from '@/components/ui/containers/Box' import {Skeleton} from '@/components/ui/feedback/Skeleton' import {useNavigation} from '@/hooks/navigation/useNavigation' import {BoatChargingRouteName} from '@/modules/boat-charging/routes' +import {useBoatChargingSettingsQuery} from '@/modules/boat-charging/service' import {formatKWH} from '@/modules/boat-charging/utils/formatKWH' import {formatNumber} from '@/utils/formatNumber' -export type BoatChargingSessionOrDummy = - | (BoatChargingSession & {dummy?: never}) - | {dummy: true; start_date_time: string} - type Props = { - session: BoatChargingSessionOrDummy + session: BoatChargingHistoryInfiniteItem } export const BoatChargingHistoryItem = memo(({session}: Props) => { const {navigate} = useNavigation() + const {data: settingsServerData} = useBoatChargingSettingsQuery() + if ('dummy' in session && session.dummy === true) { return ( @@ -38,7 +37,10 @@ export const BoatChargingHistoryItem = memo(({session}: Props) => { const elements = [ typeof session.total_cost === 'number' - ? formatNumber(session.total_cost * 1.21, session.currency) + ? formatNumber( + session.total_cost * (settingsServerData?.vat_fraction ?? 1.21), + session.currency, + ) : null, typeof session.kwh === 'number' ? formatKWH(session.kwh) : null, ].filter(Boolean) diff --git a/src/modules/boat-charging/providers/BoatChargingSessions.provider.tsx b/src/modules/boat-charging/providers/BoatChargingSessions.provider.tsx index 269b738ec..9652f6a62 100644 --- a/src/modules/boat-charging/providers/BoatChargingSessions.provider.tsx +++ b/src/modules/boat-charging/providers/BoatChargingSessions.provider.tsx @@ -1,8 +1,7 @@ import {useMemo, type ReactNode} from 'react' -import {useInterval} from '@/hooks/useInterval' import {BoatChargingSessionsContext} from '@/modules/boat-charging/hooks/useBoatChargingSessions' import {useIsLoggedIn} from '@/modules/boat-charging/hooks/useIsLoggedIn' -import {useBoatChargingSessionsQuery} from '@/modules/boat-charging/service' +import {useBoatChargingSessionsInfiniteQuery} from '@/modules/boat-charging/service' import {getActiveSessions} from '@/modules/boat-charging/utils/getActiveSessions' type Props = { @@ -16,28 +15,16 @@ export const BoatChargingSessionsProvider = ({ }: Props) => { const {isLoggedIn} = useIsLoggedIn() - const { - data, - isLoading, - isError, - refetch: refetchSessions, - } = useBoatChargingSessionsQuery( + const {data, isLoading, isError} = useBoatChargingSessionsInfiniteQuery( {}, { skip: !isLoggedIn, + pollingInterval: shouldPollSessions && isLoggedIn ? 30000 : 0, + initialPageParam: 1, }, ) - const activeSessions = getActiveSessions(data?.result) - - useInterval( - () => { - if (shouldPollSessions && isLoggedIn) { - void refetchSessions() - } - }, - shouldPollSessions && isLoggedIn ? 30000 : 0, - ) + const activeSessions = getActiveSessions(data?.pages[0].result) const value = useMemo( () => ({ diff --git a/src/modules/boat-charging/service.ts b/src/modules/boat-charging/service.ts index dbfbc01f5..eac16cfb9 100644 --- a/src/modules/boat-charging/service.ts +++ b/src/modules/boat-charging/service.ts @@ -22,6 +22,7 @@ import { import {prepareHeaders} from '@/modules/boat-charging/utils/prepareHeaders' import {ModuleSlug} from '@/modules/generated/slugs.generated' import {baseApi} from '@/services/baseApi' +import {INFINITE_QUERY_OPTIONS} from '@/services/constants' import {deviceIdHeader} from '@/services/headers' import {CacheLifetime} from '@/types/api' @@ -96,14 +97,16 @@ export const boatChargingApi = baseApi.injectEndpoints({ providesTags: ['BoatChargingSessions'], keepUnusedDataFor: CacheLifetime.minute, }), - [BoatChargingEndpointName.boatChargingSessions]: builder.query< + [BoatChargingEndpointName.boatChargingSessions]: builder.infiniteQuery< Paginated, - BoatChargingSessionsEndpointRequest + BoatChargingSessionsEndpointRequest, + number >({ - query: params => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ prepareHeaders, method: 'GET', - params, + params: {page: pageParam, ...queryArg}, slug: ModuleSlug['boat-charging'], url: '/sessions', }), @@ -202,7 +205,7 @@ export const { useBoatChargingOpenIdConnectConfigQuery, useBoatChargingTermsQuery, useBoatChargingSessionQuery, - useBoatChargingSessionsQuery, + useBoatChargingSessionsInfiniteQuery, useBoatChargingSettingsQuery, useBoatChargingInitSessionMutation, useBoatChargingSocketStatusQuery, diff --git a/src/modules/construction-work/components/PreRenderComponent.tsx b/src/modules/construction-work/components/PreRenderComponent.tsx index b6d2ebbf3..07a18e771 100644 --- a/src/modules/construction-work/components/PreRenderComponent.tsx +++ b/src/modules/construction-work/components/PreRenderComponent.tsx @@ -1,28 +1,27 @@ import {useEffect, useState} from 'react' import {useRegisterDevice} from '@/hooks/useRegisterDevice' import {config} from '@/modules/construction-work/components/projects/config' -import {useProjectsQuery} from '@/modules/construction-work/service' +import {useProjectsInfiniteQuery} from '@/modules/construction-work/service' export const PreRenderComponent = () => { const {registerDeviceIfPermitted} = useRegisterDevice() const [hasRequestedPermission, setHasRequestedPermission] = useState(false) // Use the same params as the Projects component to already have the data in the cache - const {data} = useProjectsQuery( + const {data} = useProjectsInfiniteQuery( { page_size: config.projectItemListPageSize, - page: 1, }, - {skip: hasRequestedPermission}, + {skip: hasRequestedPermission, initialPageParam: 1}, ) // At app startup, ask permission for push notifications if the user is following a project useEffect(() => { - if (data?.result?.[0].followed && !hasRequestedPermission) { + if (data?.pages[0]?.result?.[0].followed && !hasRequestedPermission) { void registerDeviceIfPermitted(true) setHasRequestedPermission(true) } - }, [data?.result, hasRequestedPermission, registerDeviceIfPermitted]) + }, [data?.pages, hasRequestedPermission, registerDeviceIfPermitted]) return null } diff --git a/src/modules/construction-work/components/projects/Project.tsx b/src/modules/construction-work/components/projects/Project.tsx index b41440767..69c6e3eac 100644 --- a/src/modules/construction-work/components/projects/Project.tsx +++ b/src/modules/construction-work/components/projects/Project.tsx @@ -41,14 +41,14 @@ export const Project = memo( ] }, [followed, meter, readArticles, recent_articles, showTraits]) - const {id, image, isDummyItem, subtitle, title} = project + const {id, image, dummy, subtitle, title} = project return ( onPress(id, isDummyItem)} + isDummyItem={dummy} + onPress={() => onPress(id, dummy)} subtitle={subtitle} testID={`ConstructionWork${id}ProjectCard`} title={title} diff --git a/src/modules/construction-work/components/projects/Projects.tsx b/src/modules/construction-work/components/projects/Projects.tsx index e8c5ce9b8..391b02067 100644 --- a/src/modules/construction-work/components/projects/Projects.tsx +++ b/src/modules/construction-work/components/projects/Projects.tsx @@ -10,10 +10,7 @@ import {getAddressParam} from '@/modules/address/utils/getAddressParam' import {ProjectsList} from '@/modules/construction-work/components/projects/ProjectsList' import {ProjectsListHeader} from '@/modules/construction-work/components/projects/ProjectsListHeader' import {config} from '@/modules/construction-work/components/projects/config' -import { - projectsApi, - useProjectsQuery, -} from '@/modules/construction-work/service' +import {projectsApi} from '@/modules/construction-work/service' import { type ProjectsItem, type ProjectsQueryArgs, @@ -28,7 +25,7 @@ const emptyProjectsItem: ProjectsListItem = { image: null, meter: 0, id: -1, - isDummyItem: true, + dummy: true, recent_articles: [], subtitle: ' ', title: ' ', @@ -46,15 +43,10 @@ export const Projects = () => { projectItemListPageSize, ) - const result = useInfiniteScroller< - ProjectsItem, - ProjectsListItem, - ProjectsQueryArgs - >( + const result = useInfiniteScroller( emptyProjectsItem, projectsApi.endpoints[ConstructionWorkEndpointName.projects], 'id', - useProjectsQuery, page, projectItemListPageSize, { diff --git a/src/modules/construction-work/service.ts b/src/modules/construction-work/service.ts index 702ef36e1..5ed8a9fdd 100644 --- a/src/modules/construction-work/service.ts +++ b/src/modules/construction-work/service.ts @@ -18,6 +18,7 @@ import {processSearchQueryArgs} from '@/modules/construction-work/utils/processS import {tempPostProcessProjectDetails} from '@/modules/construction-work/utils/tempPostProcessProjectDetails' import {ModuleSlug} from '@/modules/generated/slugs.generated' import {baseApi} from '@/services/baseApi' +import {INFINITE_QUERY_OPTIONS} from '@/services/constants' import {deviceIdHeader} from '@/services/headers' import {CacheLifetime} from '@/types/api' import {generateRequestUrl} from '@/utils/api' @@ -84,15 +85,17 @@ export const projectsApi = baseApi.injectEndpoints({ }), // /projects GET - [ConstructionWorkEndpointName.projects]: builder.query< + [ConstructionWorkEndpointName.projects]: builder.infiniteQuery< ProjectsResponse, - ProjectsQueryArgs + ProjectsQueryArgs, + number >({ providesTags: ['FollowedProjects', 'Projects'], - query: params => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ slug: MODULE_SLUG, url: '/projects', - params, + params: {page: pageParam, ...queryArg}, headers: deviceIdHeader, }), keepUnusedDataFor: CacheLifetime.hour, @@ -155,6 +158,6 @@ export const { useProjectNewsQuery, useProjectUnfollowMutation, useProjectWarningQuery, - useProjectsQuery, + useProjectsInfiniteQuery, useProjectsSearchQuery, } = projectsApi diff --git a/src/modules/construction-work/types/project.ts b/src/modules/construction-work/types/project.ts index 7a22092bb..7ae4feb24 100644 --- a/src/modules/construction-work/types/project.ts +++ b/src/modules/construction-work/types/project.ts @@ -7,7 +7,7 @@ import type { } from '@/modules/construction-work/types/api' export type ProjectsListItem = ProjectsItem & { - isDummyItem?: boolean + dummy?: boolean } export type ProjectSegment = { diff --git a/src/modules/news/components/NewsHighlights.tsx b/src/modules/news/components/NewsHighlights.tsx index 5e0a5f20d..3daa25484 100644 --- a/src/modules/news/components/NewsHighlights.tsx +++ b/src/modules/news/components/NewsHighlights.tsx @@ -2,26 +2,26 @@ import {PleaseWait} from '@/components/ui/feedback/PleaseWait' import {SomethingWentWrong} from '@/components/ui/feedback/SomethingWentWrong' import {Column} from '@/components/ui/layout/Column' import {NewsListItem} from '@/modules/news/components/NewsListItem' -import {useNewsArticlesQuery} from '@/modules/news/service' +import {useNewsArticlesInfiniteQuery} from '@/modules/news/service' export const NewsHighlights = () => { const { data: highlights, isLoading, isError, - } = useNewsArticlesQuery({type: 'highlight'}) + } = useNewsArticlesInfiniteQuery({type: 'highlight'}, {initialPageParam: 1}) if (isLoading) { return } - if (isError || !highlights?.result.length) { + if (isError || !highlights?.pages[0]?.result.length) { return } return ( - {highlights.result.map(highlight => ( + {highlights.pages[0].result.map(highlight => ( ( + const result = useInfiniteScroller( emptyNewsItem, newsApi.endpoints[NewsEndpointName.articles], 'id', - useNewsArticlesQuery, page, PAGE_SIZE, { diff --git a/src/modules/news/hooks/useHighlightedArticle.ts b/src/modules/news/hooks/useHighlightedArticle.ts index 8568f6fda..95b48b2fd 100644 --- a/src/modules/news/hooks/useHighlightedArticle.ts +++ b/src/modules/news/hooks/useHighlightedArticle.ts @@ -1,7 +1,7 @@ import {useCallback, useEffect, useMemo} from 'react' import {useDispatch} from '@/hooks/redux/useDispatch' import {useSelector} from '@/hooks/redux/useSelector' -import {useNewsArticlesQuery} from '@/modules/news/service' +import {useNewsArticlesInfiniteQuery} from '@/modules/news/service' import { selectHighlightedArticleQueue, selectHighlightedArticleStatus, @@ -15,11 +15,14 @@ export const useHighlightedArticle = () => { const highlightedArticleStatus = useSelector(selectHighlightedArticleStatus) const dispatch = useDispatch() - const {data: highlights, ...rest} = useNewsArticlesQuery({type: 'highlight'}) + const {data: highlights, ...rest} = useNewsArticlesInfiniteQuery( + {type: 'highlight'}, + {initialPageParam: 1}, + ) const advanceHighlightQueue = useCallback(() => { const incomingQueue = - highlights?.result.map(highlight => highlight.id) ?? [] + highlights?.pages[0]?.result.map(highlight => highlight.id) ?? [] const newQueue = mergeAndOrderQueue( new Set(highlightedArticleQueue), @@ -45,7 +48,7 @@ export const useHighlightedArticle = () => { ]) const highlightedArticle = useMemo(() => { - const liveblog = highlights?.result.find( + const liveblog = highlights?.pages[0]?.result.find( highlight => highlight.is_active_liveblog, ) @@ -53,10 +56,10 @@ export const useHighlightedArticle = () => { return liveblog } - return highlights?.result.find( + return highlights?.pages[0]?.result.find( highlight => highlight.id === highlightedArticleQueue[0], ) - }, [highlightedArticleQueue, highlights?.result]) + }, [highlightedArticleQueue, highlights?.pages]) return { highlightedArticle, diff --git a/src/modules/news/service.ts b/src/modules/news/service.ts index cff42616b..2776b76ae 100644 --- a/src/modules/news/service.ts +++ b/src/modules/news/service.ts @@ -9,18 +9,21 @@ import { type LiveblogResponse, } from '@/modules/news/types' import {baseApi} from '@/services/baseApi' +import {INFINITE_QUERY_OPTIONS} from '@/services/constants' import {deviceIdHeader} from '@/services/headers' export const newsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - [NewsEndpointName.articles]: builder.query< + [NewsEndpointName.articles]: builder.infiniteQuery< NewsArticlesResponse, - NewsArticlesQueryArgs + NewsArticlesQueryArgs, + number >({ - query: args => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ url: '/articles', slug: ModuleSlug.news, - params: args, + params: {page: pageParam, ...queryArg}, }), }), [NewsEndpointName.article]: builder.query({ @@ -86,7 +89,7 @@ export const newsApi = baseApi.injectEndpoints({ export const { useNewsArticleQuery, - useNewsArticlesQuery, + useNewsArticlesInfiniteQuery, useNewsLiveblogQuery, useNewsDistrictsQuery, useNewsGetLiveblogNotificationsQuery, diff --git a/src/modules/parking/components/moneyTransactionsList/ParkingMoneyTransactionsList.tsx b/src/modules/parking/components/moneyTransactionsList/ParkingMoneyTransactionsList.tsx index 141d54916..3b2b47027 100644 --- a/src/modules/parking/components/moneyTransactionsList/ParkingMoneyTransactionsList.tsx +++ b/src/modules/parking/components/moneyTransactionsList/ParkingMoneyTransactionsList.tsx @@ -9,13 +9,11 @@ import {Gutter} from '@/components/ui/layout/Gutter' import {Row} from '@/components/ui/layout/Row' import {Phrase} from '@/components/ui/text/Phrase' import {useInfiniteScroller} from '@/hooks/useInfiniteScroller' -import { - parkingApi, - useParkingTransactionsQuery, -} from '@/modules/parking/service' +import {parkingApi} from '@/modules/parking/service' import { ParkingEndpointName, ParkingOrderType, + ParkingSessionStatus, ParkingTransaction, ParkingTransactionsEndpointRequest, } from '@/modules/parking/types' @@ -31,15 +29,11 @@ const ListEmptyComponent = () => ( /> ) -type ParkingTransactionOrDummy = - | (ParkingTransaction & {dummy?: never; page: number}) - | { - created_date_time: string - dummy: true - page: number - ps_right_id: number - start_date_time: string - } +type ParkingTransactionOrDummy = ParkingTransaction & { + dummy?: boolean + page: number +} + type Section = { data: Array title: string @@ -54,7 +48,6 @@ export const ParkingMoneyTransactionsList = () => { const result = useInfiniteScroller< ParkingTransaction, - ParkingTransactionOrDummy, ParkingTransactionsEndpointRequest >( { @@ -62,11 +55,25 @@ export const ParkingMoneyTransactionsList = () => { dummy: true, ps_right_id: 0, start_date_time: '', - page: 0, + end_date_time: '', + no_endtime: false, + remaining_time: 0, + report_code: '', + status: ParkingSessionStatus.cancelled, + vehicle_id: '', + is_cancelled: false, + is_paid: false, + parking_cost: { + currency: '', + value: 0, + }, + amount: { + currency: '', + value: 0, + }, }, parkingApi.endpoints[ParkingEndpointName.parkingTransactions], 'created_date_time', - useParkingTransactionsQuery, page, pageSize, { diff --git a/src/modules/parking/components/sessionsList/ParkingSessionHistoryList.tsx b/src/modules/parking/components/sessionsList/ParkingSessionHistoryList.tsx index 461e5e654..d907ef4b4 100644 --- a/src/modules/parking/components/sessionsList/ParkingSessionHistoryList.tsx +++ b/src/modules/parking/components/sessionsList/ParkingSessionHistoryList.tsx @@ -7,15 +7,12 @@ import {Phrase} from '@/components/ui/text/Phrase' import {useInfiniteScroller} from '@/hooks/useInfiniteScroller' import {ParkingSessionListRenderItem} from '@/modules/parking/components/sessionsList/ParkingSessionListRenderItem' import {useCurrentParkingPermit} from '@/modules/parking/hooks/useCurrentParkingPermit' -import { - parkingApi, - useParkingSessionHistoryQuery, -} from '@/modules/parking/service' +import {parkingApi} from '@/modules/parking/service' import { ParkingEndpointName, ParkingHistorySession, - ParkingOrderType, ParkingSessionsEndpointRequest, + ParkingSessionStatus, } from '@/modules/parking/types' import {layoutStyles} from '@/styles/layoutStyles' import {getCurrentPage} from '@/utils/pagination/getCurrentPage' @@ -24,9 +21,10 @@ import { getSectionsSortedByDate, } from '@/utils/sort/getSectionsSortedByDate' -type ParkingHistorySessionOrDummy = - | (ParkingHistorySession & {dummy?: never}) - | {dummy: true; ps_right_id: number; start_date_time: string} +type ParkingHistorySessionOrDummy = ParkingHistorySession & { + dummy?: boolean + page: number +} type Props = { ListEmptyComponent?: ComponentType @@ -47,7 +45,6 @@ export const ParkingSessionHistoryList = ({ const result = useInfiniteScroller< ParkingHistorySession, - ParkingHistorySessionOrDummy, ParkingSessionsEndpointRequest >( { @@ -56,10 +53,25 @@ export const ParkingSessionHistoryList = ({ : '1970-01-01T00:00:00', dummy: true, ps_right_id: 0, + end_date_time: '', + no_endtime: false, + remaining_time: 0, + report_code: '', + status: ParkingSessionStatus.planned, + vehicle_id: '', + created_date_time: '', + is_cancelled: false, + parking_cost: { + currency: '', + value: 0, + }, + amount: { + currency: '', + value: 0, + }, }, parkingApi.endpoints[ParkingEndpointName.parkingSessionHistory], - 'start_date_time', - useParkingSessionHistoryQuery, + 'ps_right_id', page, pageSize, { @@ -78,9 +90,7 @@ export const ParkingSessionHistoryList = ({ >( ({viewableItems}) => { if (viewableItems.length > 0) { - const items = viewableItems - .flatMap(section => section.item) - .filter(item => item.ps_right_id) + const items = viewableItems.flatMap(section => section.item) if (items.length === 0) { return @@ -90,10 +100,10 @@ export const ParkingSessionHistoryList = ({ item => item.ps_right_id === items[0]?.ps_right_id, ) const lastIndex = result.data.findIndex( - item => item.ps_right_id === items[items.length - 1]?.ps_right_id, + item => item.ps_right_id === items.at(-1)?.ps_right_id, ) - if (firstIndex && lastIndex) { + if (firstIndex >= 0 && lastIndex >= 0) { setViewableItemIndex(Math.round((firstIndex + lastIndex) / 2)) } } @@ -101,16 +111,10 @@ export const ParkingSessionHistoryList = ({ [result.data], ) - const sections = useMemo(() => { - const sessionsOnly = result.data.filter( - item => - item.dummy || - item.order_type === undefined || // On v2 this property doesn't exist - item.order_type === ParkingOrderType.session, - ) - - return getSectionsSortedByDate(sessionsOnly, sortAscending) - }, [result, sortAscending]) + const sections = useMemo( + () => getSectionsSortedByDate(result.data, sortAscending), + [result, sortAscending], + ) return ( ( { @@ -71,7 +70,6 @@ export const ParkingSessionsList = ({ }, parkingApi.endpoints[ParkingEndpointName.parkingSessions], 'ps_right_id', - useParkingSessionsQuery, page, pageSize, { diff --git a/src/modules/parking/hooks/useGetParkingSessions.ts b/src/modules/parking/hooks/useGetParkingSessions.ts index 57cdd8cb4..188eabd5d 100644 --- a/src/modules/parking/hooks/useGetParkingSessions.ts +++ b/src/modules/parking/hooks/useGetParkingSessions.ts @@ -1,7 +1,7 @@ import {skipToken} from '@reduxjs/toolkit/query' import {useCurrentParkingPermit} from '@/modules/parking/hooks/useCurrentParkingPermit' import { - useParkingSessionsQuery, + useParkingSessionsInfiniteQuery, useVisitorParkingSessionsQuery, } from '@/modules/parking/service' import {useParkingAccount, useVisitorVehicleId} from '@/modules/parking/slice' @@ -16,11 +16,11 @@ export const useGetParkingSessions = ( const {visitorVehicleId} = useVisitorVehicleId() const { - currentData: parkingSessions, + data: parkingSessionsInfinite, isLoading: isLoadingParkingSessions, isError: isParkingSessionsError, refetch: refetchParkingSessions, - } = useParkingSessionsQuery( + } = useParkingSessionsInfiniteQuery( currentPermit && parkingAccount?.scope === ParkingPermitScope.permitHolder ? { report_code: currentPermit.report_code.toString(), @@ -28,8 +28,9 @@ export const useGetParkingSessions = ( page_size: 100, } : skipToken, - options, + {initialPageParam: 1, ...options}, ) + const parkingSessions = parkingSessionsInfinite?.pages[0] const { currentData: visitorParkingSessions, diff --git a/src/modules/parking/service.ts b/src/modules/parking/service.ts index 9e8705523..8dc052622 100644 --- a/src/modules/parking/service.ts +++ b/src/modules/parking/service.ts @@ -42,6 +42,7 @@ import { import {afterError} from '@/modules/parking/utils/afterError' import {prepareHeaders} from '@/modules/parking/utils/prepareHeaders' import {baseApi} from '@/services/baseApi' +import {INFINITE_QUERY_OPTIONS} from '@/services/constants' import {deviceIdHeader} from '@/services/headers' import {CacheLifetime} from '@/types/api' import {generateRequestUrl} from '@/utils/api' @@ -116,45 +117,51 @@ export const parkingApi = baseApi.injectEndpoints({ }, }), }), - [ParkingEndpointName.parkingSessionHistory]: builder.query< + [ParkingEndpointName.parkingSessionHistory]: builder.infiniteQuery< ParkingSessionHistoryEndpointResponse, - ParkingSessionHistoryEndpointRequest + ParkingSessionHistoryEndpointRequest, + number >({ providesTags: ['ParkingSessions'], - query: params => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ prepareHeaders, method: 'GET', - params, + params: {page: pageParam, ...queryArg}, slug: ModuleSlug.parking, url: '/sessions/history', afterError, }), keepUnusedDataFor: CacheLifetime.second * 3, }), - [ParkingEndpointName.parkingSessions]: builder.query< + [ParkingEndpointName.parkingSessions]: builder.infiniteQuery< ParkingSessionsEndpointResponse, - ParkingSessionsEndpointRequest + ParkingSessionsEndpointRequest, + number >({ providesTags: ['ParkingSessions'], - query: ({...params}) => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ prepareHeaders, method: 'GET', - params, + params: {page: pageParam, ...queryArg}, slug: ModuleSlug.parking, url: '/sessions', afterError, }), keepUnusedDataFor: CacheLifetime.hour, }), - [ParkingEndpointName.parkingTransactions]: builder.query< + [ParkingEndpointName.parkingTransactions]: builder.infiniteQuery< ParkingTransactionsEndpointResponse, - ParkingTransactionsEndpointRequest + ParkingTransactionsEndpointRequest, + number >({ providesTags: ['ParkingTransactions'], - query: ({...params}) => ({ + infiniteQueryOptions: INFINITE_QUERY_OPTIONS, + query: ({pageParam, queryArg = {}}) => ({ prepareHeaders, method: 'GET', - params, + params: {page: pageParam, ...queryArg}, slug: ModuleSlug.parking, url: '/transactions', afterError, @@ -411,9 +418,9 @@ export const { useConfirmBalanceMutation, useLicensePlatesQuery, useLoginMutation: useLoginParkingMutation, - useParkingSessionHistoryQuery, - useParkingSessionsQuery, - useParkingTransactionsQuery, + useParkingSessionHistoryInfiniteQuery, + useParkingSessionsInfiniteQuery, + useParkingTransactionsInfiniteQuery, useRemoveLicensePlateMutation, useParkingMachinesQuery, useZoneByMachineQuery, diff --git a/src/modules/parking/types.ts b/src/modules/parking/types.ts index d697ae12f..52144fc36 100644 --- a/src/modules/parking/types.ts +++ b/src/modules/parking/types.ts @@ -405,9 +405,9 @@ export type ParkingManageVisitorTimeBalanceEndpointRequest = { seconds_to_transfer: number } -export type ParkingSessionOrDummy = - | ((ParkingSession | VisitorParkingSession) & {dummy?: never}) - | {dummy: true; ps_right_id: number; start_date_time: string} +export type ParkingSessionOrDummy = (ParkingSession | VisitorParkingSession) & { + dummy?: boolean +} export type ParkingZoneByMachineEndpointRequest = { machineId: string diff --git a/src/services/constants.ts b/src/services/constants.ts new file mode 100644 index 000000000..ad598d305 --- /dev/null +++ b/src/services/constants.ts @@ -0,0 +1,14 @@ +import type {Paginated} from '@/types/api' +import type {InfiniteQueryConfigOptions} from '@reduxjs/toolkit/query' + +export const INFINITE_QUERY_OPTIONS: InfiniteQueryConfigOptions< + Paginated, + number, + unknown +> = { + initialPageParam: 1, + getNextPageParam: (lastPage, _allPages, lastPageParam) => + lastPage.page.totalPages > lastPageParam ? lastPageParam + 1 : undefined, + getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => + firstPageParam > 1 ? firstPageParam - 1 : undefined, +} diff --git a/src/services/deviceRegistration.service.ts b/src/services/deviceRegistration.service.ts index 6465c6e9e..3a837b56b 100644 --- a/src/services/deviceRegistration.service.ts +++ b/src/services/deviceRegistration.service.ts @@ -2,17 +2,24 @@ import {Platform} from 'react-native' import {GlobalApiSlug} from '@/environment' import {baseApi} from '@/services/baseApi' import {deviceIdHeader} from '@/services/headers' -import {MutationResponse} from '@/types/api' import {DeviceRegistrationEndpointName} from '@/types/device' type DeviceRegistrationQueryArg = { firebase_token: string } +type DeviceRegistrationMutationResponse = { + external_id: string + firebase_token: string + id: number + last_seen: string + os: string +} + export const deviceRegistrationApi = baseApi.injectEndpoints({ endpoints: builder => ({ [DeviceRegistrationEndpointName.registerDevice]: builder.mutation< - MutationResponse, + DeviceRegistrationMutationResponse, DeviceRegistrationQueryArg >({ query: body => ({ @@ -27,7 +34,7 @@ export const deviceRegistrationApi = baseApi.injectEndpoints({ }), }), [DeviceRegistrationEndpointName.unregisterDevice]: builder.mutation< - MutationResponse, + string, undefined >({ query: () => ({ diff --git a/src/types/api.ts b/src/types/api.ts index ca440946f..fd0879aa0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,8 +1,17 @@ -/** @deprecated API refactor: this is no longer the default mutation response, will be replaced by string */ -export type MutationResponse = { - result: string - status: boolean -} +import { + type BaseQueryFn, + type EndpointDefinitions, + type FetchArgs, + type FetchBaseQueryError, + type InfiniteQueryDefinition, +} from '@reduxjs/toolkit/query' +import type {ApiSlug} from '@/environment' +import type { + ApiEndpointInfiniteQuery, + TypedUseInfiniteQuery, + TypedUseInfiniteQueryState, + TypedUseInfiniteQuerySubscription, +} from '@reduxjs/toolkit/query/react' type Links = { next: {href: string} @@ -53,7 +62,7 @@ export type AddressQueryArgs = { } export type PaginationQueryArgs = { - page?: number + // page?: number is omitted because it is handled by the infinite query hook page_size?: number } @@ -72,3 +81,36 @@ export type ApiError = { code: CodesEnum detail: string } + +export type ApiEndpointInfinite< + Item, + QueryArgs extends PaginationQueryArgs, +> = ApiEndpointInfiniteQuery< + InfiniteQueryDefinition< + QueryArgs, + number, + BaseQueryFn, + string, + Paginated + >, + EndpointDefinitions +> & { + useInfiniteQuery: TypedUseInfiniteQuery< + Paginated, + QueryArgs, + number, + BaseQueryFn + > + useInfiniteQueryState: TypedUseInfiniteQueryState< + Paginated, + QueryArgs, + number, + BaseQueryFn + > + useInfiniteQuerySubscription: TypedUseInfiniteQuerySubscription< + Paginated, + QueryArgs, + number, + BaseQueryFn + > +}