Skip to content

Commit fdc684f

Browse files
authored
Merge pull request #124 from Berkeley-CS61B/stats-loading
Stats loading
2 parents c582e5e + 624a14c commit fdc684f

File tree

2 files changed

+176
-27
lines changed

2 files changed

+176
-27
lines changed

src/components/activity/StatsView.tsx

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState } from 'react';
2-
import { Flex, Grid, GridItem, Spinner, Text } from '@chakra-ui/react';
1+
import { useEffect, useState } from 'react';
2+
import { Flex, Grid, GridItem, Input, Spinner, Text } from '@chakra-ui/react';
33
import { Select } from 'chakra-react-select';
44
import StatsGraph from './StatsGraph';
55
import { trpc } from '../../utils/trpc';
@@ -90,42 +90,87 @@ const StatsView = () => {
9090
const [personalTicketStats, setPersonalTicketStats] = useState<TicketStats[]>([]);
9191
const [globalTimeRangeOption, setGlobalTimeRangeOption] = useState(timeRangeOptions[0]);
9292
const [personalTimeRangeOption, setPersonalTimeRangeOption] = useState(timeRangeOptions[0]);
93+
const [globalStartDate, setGlobalStartDate] = useState<Date>();
94+
const [personalStartDate, setPersonalStartDate] = useState<Date>();
9395

94-
const { isLoading: isStatsLoading } = trpc.stats.getTicketStats.useQuery(undefined, {
96+
const { isFetching: isFetchingStats, fetchNextPage: fetchNextStatsPage, hasNextPage: statsHasNextPage } =
97+
trpc.stats.getInfiniteTicketStats.useInfiniteQuery({ limit: 10000 }, {
9598
refetchOnWindowFocus: false,
9699
onSuccess: data => {
97-
setTicketStats(data);
100+
setTicketStats(data.pages.map(p => p.items).reduce((a, b) => a.concat(b)));
98101
},
102+
getNextPageParam: (lastPage) => lastPage.nextCursor
99103
});
100-
const { isLoading: isPersonalStatsLoading } = trpc.stats.getTicketStatsHelpedByUser.useQuery(undefined, {
104+
105+
useEffect(() => {
106+
if (statsHasNextPage) {
107+
fetchNextStatsPage();
108+
}
109+
}, [ticketStats])
110+
111+
const { isFetching: isFetchingPersonalStats, fetchNextPage: fetchNextPersonalStatsPage, hasNextPage: personalStatsHasNextPage } =
112+
trpc.stats.getInfiniteTicketStats.useInfiniteQuery({ limit: 10000 }, {
101113
refetchOnWindowFocus: false,
102114
onSuccess: data => {
103-
setPersonalTicketStats(data);
115+
setPersonalTicketStats(data.pages.map(p => p.items).reduce((a, b) => a.concat(b)));
104116
},
117+
getNextPageParam: (lastPage) => lastPage.nextCursor
105118
});
106119

107-
const getTimeRange = (timeRangeOption: TimeRangeType | undefined, end: Date): TimeRange | undefined => {
108-
let start = new Date(end);
120+
useEffect(() => {
121+
if (personalStatsHasNextPage) {
122+
fetchNextPersonalStatsPage();
123+
}
124+
}, [personalTicketStats])
125+
126+
const isStatsLoading = isFetchingStats || statsHasNextPage || isFetchingPersonalStats || personalStatsHasNextPage;
127+
128+
const getStartDateFromCurrent = (timeRangeOption: TimeRangeType) => {
129+
let start = new Date();
130+
switch (timeRangeOption.type) {
131+
case 'day':
132+
start.setDate(start.getDate() - 1);
133+
break;
134+
case 'week':
135+
start.setDate(start.getDate() - 7);
136+
break;
137+
case 'month':
138+
start.setMonth(start.getMonth() - 1);
139+
break;
140+
case 'all':
141+
start = new Date('January 1, 2023 00:00:00');
142+
break;
143+
default:
144+
break;
145+
}
146+
return start;
147+
}
148+
149+
const getTimeRange = (timeRangeOption: TimeRangeType | undefined, startDate: Date | undefined): TimeRange | undefined => {
109150
if (!timeRangeOption) {
110151
return undefined;
111152
}
153+
let start = new Date();
154+
if (startDate !== undefined) {
155+
start = startDate;
156+
} else {
157+
start = getStartDateFromCurrent(timeRangeOption);
158+
}
159+
160+
const end = new Date(start);
112161
switch (timeRangeOption.type) {
113162
case 'day':
114-
start.setDate(end.getDate() - 1);
163+
end.setDate(start.getDate() + 1);
115164
return { type: timeRangeOption, startTime: start, endTime: end };
116165
case 'week':
117-
start.setDate(end.getDate() - 7);
118-
start.setHours(0, 0, 0, 0); // Round down start date
119-
end.setHours(23, 59, 59, 999); // Round up end date
166+
end.setDate(start.getDate() + 7);
120167
return { type: timeRangeOption, startTime: start, endTime: end };
121168
case 'month':
122-
start.setMonth(end.getMonth() - 1);
123-
start.setHours(0, 0, 0, 0); // Round down start date
124-
end.setHours(23, 59, 59, 999); // Round up end date
169+
end.setMonth(start.getMonth() + 1);
125170
return { type: timeRangeOption, startTime: start, endTime: end };
126171
case 'all':
127172
start = new Date('January 1, 2023 00:00:00');
128-
return { type: timeRangeOption, startTime: start, endTime: end };
173+
return { type: timeRangeOption, startTime: start, endTime: new Date() };
129174
default:
130175
return { type: timeRangeOption, startTime: start, endTime: end };
131176
}
@@ -134,10 +179,22 @@ const StatsView = () => {
134179
return (
135180
<Grid m={4} h='100%' w='auto' templateRows='30px 1fr 30px 1fr' templateColumns='repeat(6, 1fr)' gap={4}>
136181
<GridItem rowSpan={1} colSpan={6}>
137-
<Flex justifyContent='space-between'>
138-
<Text fontSize='3xl' fontWeight='semibold' mb={3}>
182+
<Flex justifyContent='flex-end' alignItems='center'>
183+
<Text fontSize='3xl' fontWeight='semibold' mb={3} mr="auto">
139184
Global Statistics
140185
</Text>
186+
<Text mr={3}>Start Date:</Text>
187+
<Input
188+
mr={3}
189+
width="250px"
190+
placeholder="Select Date and Time"
191+
size="md"
192+
type="datetime-local"
193+
onChange={event =>
194+
setGlobalStartDate(event.target.value === "" ? undefined : new Date(event.target.value))
195+
}
196+
/>
197+
<Text mr={3}>Range:</Text>
141198
<Select
142199
value={globalTimeRangeOption}
143200
onChange={val => setGlobalTimeRangeOption(val ?? undefined)}
@@ -152,20 +209,32 @@ const StatsView = () => {
152209
</Flex>
153210
</GridItem>
154211
<GridItem rowSpan={1} colSpan={4}>
155-
<StatsGraph timeRange={getTimeRange(globalTimeRangeOption?.value, new Date())} stats={ticketStats} />
212+
<StatsGraph timeRange={getTimeRange(globalTimeRangeOption?.value, globalStartDate)} stats={ticketStats} />
156213
</GridItem>
157214
<GridItem mt={4} rowSpan={1} colSpan={2}>
158-
{isStatsLoading || isPersonalStatsLoading ? (
215+
{isStatsLoading ? (
159216
<Spinner />
160217
) : (
161-
<StatsPanel timeRange={getTimeRange(globalTimeRangeOption?.value, new Date())} stats={ticketStats} />
218+
<StatsPanel timeRange={getTimeRange(globalTimeRangeOption?.value, globalStartDate)} stats={ticketStats} />
162219
)}
163220
</GridItem>
164221
<GridItem rowSpan={1} colSpan={6}>
165-
<Flex justifyContent='space-between'>
166-
<Text fontSize='3xl' fontWeight='semibold' mb={3}>
222+
<Flex justifyContent='flex-end' alignItems="center">
223+
<Text fontSize='3xl' fontWeight='semibold' mb={3} mr="auto">
167224
Personal Statistics
168225
</Text>
226+
<Text mr={3}>Start Date:</Text>
227+
<Input
228+
mr={3}
229+
width="250px"
230+
placeholder="Select Date and Time"
231+
size="md"
232+
type="datetime-local"
233+
onChange={event =>
234+
setPersonalStartDate(event.target.value === "" ? undefined : new Date(event.target.value))
235+
}
236+
/>
237+
<Text mr={3}>Range:</Text>
169238
<Select
170239
value={personalTimeRangeOption}
171240
onChange={val => setPersonalTimeRangeOption(val ?? undefined)}
@@ -180,14 +249,14 @@ const StatsView = () => {
180249
</Flex>
181250
</GridItem>
182251
<GridItem rowSpan={1} colSpan={4}>
183-
<StatsGraph timeRange={getTimeRange(personalTimeRangeOption?.value, new Date())} stats={personalTicketStats} />
252+
<StatsGraph timeRange={getTimeRange(personalTimeRangeOption?.value, personalStartDate)} stats={personalTicketStats} />
184253
</GridItem>
185254
<GridItem mt={4} rowSpan={1} colSpan={2}>
186-
{isStatsLoading || isPersonalStatsLoading ? (
255+
{isStatsLoading ? (
187256
<Spinner />
188257
) : (
189258
<StatsPanel
190-
timeRange={getTimeRange(personalTimeRangeOption?.value, new Date())}
259+
timeRange={getTimeRange(personalTimeRangeOption?.value, personalStartDate)}
191260
stats={personalTicketStats}
192261
/>
193262
)}

src/server/trpc/router/stats.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@ export const statsRouter = router({
1818
},
1919
});
2020
}),
21+
getInfiniteTicketStats: publicProcedure
22+
.input(z.object({
23+
limit: z.number().min(1).max(10000).nullish(),
24+
cursor: z.number().nullish(),
25+
}))
26+
.query(async ({ input, ctx }) => {
27+
const limit = input.limit ?? 1000;
28+
const { cursor } = input;
29+
const items = await ctx.prisma.ticket.findMany({
30+
take: limit + 1,
31+
select: {
32+
id: true,
33+
createdAt: true,
34+
helpedAt: true,
35+
resolvedAt: true,
36+
status: true,
37+
ticketType: true,
38+
description: true,
39+
isPublic: true,
40+
locationId: true,
41+
assignmentId: true,
42+
},
43+
cursor: cursor ? { id: cursor } : undefined,
44+
orderBy: {
45+
id: 'asc',
46+
},
47+
})
48+
let nextCursor: typeof cursor | undefined = undefined;
49+
if (items.length > limit) {
50+
const nextItem = items.pop()
51+
nextCursor = nextItem?.id;
52+
}
53+
return {
54+
items,
55+
nextCursor,
56+
};
57+
}
58+
),
2159
getTicketStatsHelpedByUser: protectedNotStudentProcedure
2260
.query(async ({ ctx }) => {
2361
return ctx.prisma.ticket.findMany({
@@ -36,7 +74,49 @@ export const statsRouter = router({
3674
helpedByUserId: ctx.session?.user?.id
3775
},
3876
});
39-
}),
77+
}
78+
),
79+
getInfiniteTicketStatsHelpedByUser: publicProcedure
80+
.input(z.object({
81+
limit: z.number().min(1).max(10000).nullish(),
82+
cursor: z.number().nullish(),
83+
}))
84+
.query(async ({ input, ctx }) => {
85+
const limit = input.limit ?? 1000;
86+
const { cursor } = input;
87+
const items = await ctx.prisma.ticket.findMany({
88+
take: limit + 1,
89+
select: {
90+
id: true,
91+
createdAt: true,
92+
helpedAt: true,
93+
resolvedAt: true,
94+
status: true,
95+
ticketType: true,
96+
description: true,
97+
isPublic: true,
98+
locationId: true,
99+
assignmentId: true,
100+
},
101+
where: {
102+
helpedByUserId: ctx.session?.user?.id
103+
},
104+
cursor: cursor ? { id: cursor } : undefined,
105+
orderBy: {
106+
id: 'asc',
107+
},
108+
})
109+
let nextCursor: typeof cursor | undefined = undefined;
110+
if (items.length > limit) {
111+
const nextItem = items.pop()
112+
nextCursor = nextItem?.id;
113+
}
114+
return {
115+
items,
116+
nextCursor,
117+
};
118+
}
119+
),
40120
});
41121

42122
export interface TicketStats {

0 commit comments

Comments
 (0)