From 1a006438555d3000cdf5d1e7445d513d07644f6b Mon Sep 17 00:00:00 2001 From: Graham Mather Date: Tue, 26 Nov 2024 17:34:26 -0500 Subject: [PATCH 1/4] Admin Charts --- chartjs-adapter-date-fns.d.ts | 1 + package.json | 4 + src/components/Admin/AdminData.tsx | 39 +++ src/components/Admin/BarChartOnboarding.tsx | 113 +++++++++ src/components/Admin/LineChartCount.tsx | 260 ++++++++++++++++++++ src/components/Admin/UserManagement.tsx | 6 +- src/pages/admin.tsx | 7 +- src/server/router/createRouter.ts | 16 ++ src/server/router/user.ts | 4 +- src/server/router/user/admin.ts | 53 +++- src/utils/adminDataUtils.ts | 50 ++++ src/utils/types.ts | 17 +- yarn.lock | 40 +++ 13 files changed, 587 insertions(+), 23 deletions(-) create mode 100644 chartjs-adapter-date-fns.d.ts create mode 100644 src/components/Admin/AdminData.tsx create mode 100644 src/components/Admin/BarChartOnboarding.tsx create mode 100644 src/components/Admin/LineChartCount.tsx create mode 100644 src/utils/adminDataUtils.ts diff --git a/chartjs-adapter-date-fns.d.ts b/chartjs-adapter-date-fns.d.ts new file mode 100644 index 00000000..1a8ad33a --- /dev/null +++ b/chartjs-adapter-date-fns.d.ts @@ -0,0 +1 @@ +declare module "chartjs-adapter-date-fns"; diff --git a/package.json b/package.json index f767db1a..8bdd735f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,9 @@ "@types/styled-components": "^5.1.26", "antd": "^5.22.1", "axios": "^0.27.2", + "chart.js": "^4.4.6", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.2.0", "date-fns": "^4.1.0", "dayjs": "1.11.13", "envsafe": "^2.0.3", @@ -63,6 +66,7 @@ "rc-picker": "^4.8.1", "rc-trigger": "^5.3.4", "react": "^18.3.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", "react-easy-crop": "^5.1.0", "react-hook-form": "^7.32.2", diff --git a/src/components/Admin/AdminData.tsx b/src/components/Admin/AdminData.tsx new file mode 100644 index 00000000..05bd4757 --- /dev/null +++ b/src/components/Admin/AdminData.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from "react"; +import Spinner from "../Spinner"; +import { trpc } from "../../utils/trpc"; +import { TempUser, TempGroup } from "../../utils/types"; +import BarChartOnboarding from "./BarChartOnboarding"; +import LineChartCount from "./LineChartCount"; + +function AdminData() { + const [loading, setLoading] = useState(true); + const { data: users = [] } = + trpc.user.admin.getAllUsers.useQuery(); + const { data: groups = [] } = + trpc.user.admin.getCarpoolGroups.useQuery(); + + useEffect(() => { + if (users && groups) { + setLoading(false); + } + }, [users, groups]); + + if (loading) { + return ; + } + + return ( +
+
+
+ +
+
+ +
+
+
+ ); +} + +export default AdminData; diff --git a/src/components/Admin/BarChartOnboarding.tsx b/src/components/Admin/BarChartOnboarding.tsx new file mode 100644 index 00000000..31cabdf7 --- /dev/null +++ b/src/components/Admin/BarChartOnboarding.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { Bar } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + ChartData, + ChartOptions, +} from "chart.js"; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +import { TempUser } from "../../utils/types"; + +interface BarChartOnboardingProps { + users: TempUser[]; +} + +function BarChartOnboarding({ users }: BarChartOnboardingProps) { + const countOnboarded = users.filter((user) => user.isOnboarded).length; + const countNotOnboarded = users.length - countOnboarded; + + const barData: ChartData<"bar"> = { + labels: ["Users"], + datasets: [ + { + label: "Onboarded", + data: [countOnboarded], + backgroundColor: "#C8102E", + }, + { + label: "Not Onboarded", + data: [countNotOnboarded], + backgroundColor: "#D3D3D3", + }, + ], + }; + + const barOptions: ChartOptions<"bar"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top", + labels: { + font: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "bold", + }, + }, + }, + title: { + display: true, + text: "User Onboarding Status", + font: { + family: "Montserrat", + size: 18, + style: "normal", + weight: "bold", + }, + color: "#000000", + }, + }, + scales: { + x: { + ticks: { + font: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "bold", + }, + }, + }, + y: { + beginAtZero: true, + title: { + display: true, + text: "Number of Users", + font: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "bold", + }, + }, + }, + }, + }; + + return ( +
+
+ +
+
+ ); +} + +export default BarChartOnboarding; diff --git a/src/components/Admin/LineChartCount.tsx b/src/components/Admin/LineChartCount.tsx new file mode 100644 index 00000000..96af50d5 --- /dev/null +++ b/src/components/Admin/LineChartCount.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useState } from "react"; +import { Line } from "react-chartjs-2"; +import { + Chart as ChartJS, + TimeScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + CategoryScale, + ChartData, + ChartOptions, +} from "chart.js"; +import "chartjs-adapter-date-fns"; +import { format, startOfWeek, differenceInWeeks, addWeeks } from "date-fns"; +import { Slider } from "antd"; + +ChartJS.register( + CategoryScale, + TimeScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +import { TempUser, TempGroup } from "../../utils/types"; +import { + countCumulativeItemsPerWeek, + filterItemsByDate, +} from "../../utils/adminDataUtils"; + +interface LineChartCountProps { + users: TempUser[]; + groups: TempGroup[]; +} + +function LineChartCount({ users, groups }: LineChartCountProps) { + const [sliderRange, setSliderRange] = useState([0, 0]); + const [minDate, setMinDate] = useState(0); + const [maxDate, setMaxDate] = useState(0); + + useEffect(() => { + if (users && groups) { + const allTimestamps = [ + ...users.map((user) => user.dateCreated.getTime()), + ...groups.map((group) => group.dateCreated.getTime()), + ]; + + if (allTimestamps.length > 0) { + const minTimestamp = Math.min(...allTimestamps); + const maxTimestamp = Math.max(...allTimestamps); + + setMinDate(minTimestamp); + setMaxDate(maxTimestamp); + setSliderRange([minTimestamp, maxTimestamp]); + } + } + }, [users, groups]); + + const onSliderChange = (value: number[]) => { + setSliderRange(value); + }; + + // Filter items based on slider range + const filteredUsers = filterItemsByDate( + users, + sliderRange[0], + sliderRange[1] + ); + const filteredGroups = filterItemsByDate( + groups, + sliderRange[0], + sliderRange[1] + ); + + // Generate week labels + const allDates = [ + ...filteredUsers.map((user) => user.dateCreated), + ...filteredGroups.map((group) => group.dateCreated), + ]; + + let weekLabels: Date[] = []; + if (allDates.length > 0) { + const minWeekDate = startOfWeek( + new Date(Math.min(...allDates.map((date) => date.getTime()))), + { weekStartsOn: 1 } + ); + const maxWeekDate = startOfWeek( + new Date(Math.max(...allDates.map((date) => date.getTime()))), + { weekStartsOn: 1 } + ); + + const weeksDifference = differenceInWeeks(maxWeekDate, minWeekDate) + 1; + + for (let i = 0; i < weeksDifference; i++) { + const weekStart = addWeeks(minWeekDate, i); + weekLabels.push(weekStart); + } + } + + const userCounts = countCumulativeItemsPerWeek(filteredUsers, weekLabels); + const groupCounts = countCumulativeItemsPerWeek(filteredGroups, weekLabels); + + const lineData: ChartData<"line"> = { + labels: weekLabels, + datasets: [ + { + label: "Users", + data: userCounts, + fill: false, + backgroundColor: "#C8102E", + borderColor: "#C8102E", + tension: 0.1, + pointRadius: 10, + spanGaps: true, + }, + { + label: "Groups", + data: groupCounts, + fill: false, + showLine: true, + backgroundColor: "#C7EFB3", + borderColor: "#C7EFB3", + tension: 0.1, + pointRadius: 10, + spanGaps: true, + }, + ], + }; + + const lineOptions: ChartOptions<"line"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top", + labels: { + font: { + family: "Montserrat", + size: 18, + style: "normal", + weight: "bold", + }, + }, + }, + title: { + display: true, + text: "Cumulative Users and Groups Over Time", + font: { + family: "Montserrat", + size: 18, + style: "normal", + weight: "bold", + }, + color: "#000000", + }, + tooltip: { + enabled: true, + titleFont: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "normal", + }, + bodyFont: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "normal", + }, + }, + }, + scales: { + x: { + type: "time", + time: { + unit: "week", + tooltipFormat: "MMM dd, yyyy", + displayFormats: { week: "MMM dd" }, + }, + title: { + display: true, + text: "Date", + font: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "bold", + }, + }, + ticks: { + font: { + family: "Montserrat", + size: 12, + style: "normal", + weight: "normal", + }, + }, + }, + y: { + beginAtZero: true, + title: { + display: true, + text: "Cumulative Count", + font: { + family: "Montserrat", + size: 16, + style: "normal", + weight: "bold", + }, + }, + ticks: { + font: { + family: "Montserrat", + size: 12, + style: "normal", + weight: "normal", + }, + color: "#000000", + }, + }, + }, + }; + + const formatter = (value: any) => format(new Date(value), "MMM dd, yyyy"); + + return ( +
+ {allDates.length > 0 ? ( +
+ +
+ ) : ( +
No data available for the selected date range.
+ )} +
+ +
+ {format(new Date(sliderRange[0]), "MMM dd, yyyy")} + {format(new Date(sliderRange[1]), "MMM dd, yyyy")} +
+
+
+ ); +} + +export default LineChartCount; diff --git a/src/components/Admin/UserManagement.tsx b/src/components/Admin/UserManagement.tsx index a963798e..15d432e1 100644 --- a/src/components/Admin/UserManagement.tsx +++ b/src/components/Admin/UserManagement.tsx @@ -5,12 +5,8 @@ import Spinner from "../Spinner"; import { toast } from "react-toastify"; import { ConfigProvider, Select } from "antd"; import { Note } from "../../styles/profile"; +import { TempUser } from "../../utils/types"; -type TempUser = { - id: string; - email: string; - permission: Permission; -}; type UserManagementProps = { permission: Permission; }; diff --git a/src/pages/admin.tsx b/src/pages/admin.tsx index 4908bcb8..eaadad17 100644 --- a/src/pages/admin.tsx +++ b/src/pages/admin.tsx @@ -6,6 +6,7 @@ import { useState } from "react"; import UserManagement from "../components/Admin/UserManagement"; import Spinner from "../components/Spinner"; import { Permission } from "@prisma/client"; +import AdminData from "../components/Admin/AdminData"; export async function getServerSideProps(context: GetServerSidePropsContext) { const session = await getSession(context); @@ -47,13 +48,15 @@ const Admin: NextPage = ({ userPermission }) => { {!userPermission ? ( ) : ( -
+
- {option === "management" && ( + {option === "management" ? ( + ) : ( + )}
diff --git a/src/server/router/createRouter.ts b/src/server/router/createRouter.ts index 9a6f6ff5..1d56c598 100644 --- a/src/server/router/createRouter.ts +++ b/src/server/router/createRouter.ts @@ -22,5 +22,21 @@ const isProtected = middleware(({ ctx, next }) => { }, }); }); +const isAdmin = middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + if (ctx.session.user.permission === "USER") { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next({ + ctx: { + ...ctx, + session: ctx.session, + }, + }); +}); export const protectedRouter = procedure.use(isProtected); +export const adminRouter = procedure.use(isAdmin); diff --git a/src/server/router/user.ts b/src/server/router/user.ts index 740b88c1..6b26b58e 100644 --- a/src/server/router/user.ts +++ b/src/server/router/user.ts @@ -15,7 +15,7 @@ import { generatePresignedUrl, getPresignedImageUrl, } from "../../utils/uploadToS3"; -import { adminRouter } from "./user/admin"; +import { adminDataRouter } from "./user/admin"; const getPresignedDownloadUrlInput = z.object({ userId: z.string().optional(), }); @@ -156,5 +156,5 @@ export const userRouter = router({ requests: requestsRouter, groups: groupsRouter, emails: emailsRouter, - admin: adminRouter, + admin: adminDataRouter, }); diff --git a/src/server/router/user/admin.ts b/src/server/router/user/admin.ts index 123d1e84..9fb5c884 100644 --- a/src/server/router/user/admin.ts +++ b/src/server/router/user/admin.ts @@ -1,17 +1,11 @@ -import { protectedRouter, router } from "../createRouter"; +import { adminRouter, router } from "../createRouter"; import { z } from "zod"; -import { Permission } from "@prisma/client"; - +import { Permission, Status } from "@prisma/client"; +import { Role } from "@prisma/client"; // Router for admin dashboard queries, only Managers can edit roles // User must be Manager or Admin to view user data -export const adminRouter = router({ - getAllUsers: protectedRouter.query(async ({ ctx, input }) => { - const permission = ctx.session.user?.permission; - console.log(ctx.session); - if (permission === "USER") { - throw new Error("Unauthorized access."); - } - +export const adminDataRouter = router({ + getAllUsers: adminRouter.query(async ({ ctx, input }) => { return ctx.prisma.user.findMany({ where: { email: { @@ -22,10 +16,45 @@ export const adminRouter = router({ id: true, email: true, permission: true, + isOnboarded: true, + dateCreated: true, + }, + }); + }), + getCarpoolGroups: adminRouter.query(async ({ ctx, input }) => { + return ctx.prisma.carpoolGroup.findMany({ + where: { + AND: [ + { + users: { + some: { + role: Role.DRIVER, + status: Status.ACTIVE, + }, + }, + }, + { + users: { + some: { + role: Role.RIDER, + status: Status.ACTIVE, + }, + }, + }, + ], + }, + select: { + id: true, + dateCreated: true, + _count: { + select: { + users: true, + }, + }, }, }); }), - updateUserPermission: protectedRouter + updateUserPermission: adminRouter .input( z.object({ userId: z.string(), diff --git a/src/utils/adminDataUtils.ts b/src/utils/adminDataUtils.ts new file mode 100644 index 00000000..b2461aff --- /dev/null +++ b/src/utils/adminDataUtils.ts @@ -0,0 +1,50 @@ +import { addWeeks } from "date-fns"; + +interface ItemWithDate { + dateCreated: Date; +} +export const filterItemsByDate = ( + items: ItemWithDate[], + startTimestamp: number, + endTimestamp: number +) => { + return items.filter((item) => { + const itemTimestamp = item.dateCreated.getTime(); + return itemTimestamp >= startTimestamp && itemTimestamp <= endTimestamp; + }); +}; +export const countCumulativeItemsPerWeek = ( + items: ItemWithDate[], + weekLabels: Date[] +): (number | null)[] => { + const counts: (number | null)[] = []; + let cumulativeCount = 0; + let prevCount = 0; + let itemIndex = 0; + const sortedItems = items + .slice() + .sort( + (a, b) => + new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime() + ); + + weekLabels.forEach((weekStart, index) => { + const weekEnd = addWeeks(weekStart, 1); + while ( + itemIndex < sortedItems.length && + new Date(sortedItems[itemIndex].dateCreated) < weekEnd + ) { + cumulativeCount++; + itemIndex++; + } + + if (index === 0 || cumulativeCount > prevCount) { + counts.push(cumulativeCount); + } else { + counts.push(null); + } + prevCount = cumulativeCount; + }); + + return counts; +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 9f5a0311..48f9b66b 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,11 +1,24 @@ -import { Role } from "@prisma/client"; +import { Permission, Role } from "@prisma/client"; import { Status } from "@prisma/client"; import { Feature } from "geojson"; import type { AppRouter } from "../server/router"; import { inferRouterOutputs } from "@trpc/server"; type RouterOutput = inferRouterOutputs; - +export type TempUser = { + id: string; + email: string; + permission: Permission; + isOnboarded: boolean; + dateCreated: Date; +}; +export type TempGroup = { + id: string; + dateCreated: Date; + _count: { + users: number; + }; +}; export type PoiData = { location: string; coordLng: number; diff --git a/yarn.lock b/yarn.lock index 468d8d6a..577fb637 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1607,6 +1607,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" + integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== + "@mapbox/geojson-rewind@^0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a" @@ -2666,6 +2671,11 @@ dependencies: "@types/node" "*" +"@types/hammerjs@^2.0.45": + version "2.0.46" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1" + integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw== + "@types/hoist-non-react-statics@*": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" @@ -3464,6 +3474,26 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chart.js@^4.4.6: + version "4.4.6" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.6.tgz#da39b84ca752298270d4c0519675c7659936abec" + integrity sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA== + dependencies: + "@kurkle/color" "^0.3.0" + +chartjs-adapter-date-fns@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5" + integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg== + +chartjs-plugin-zoom@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz#79928acf2d22bd60b5442e0ef73139b261c65d19" + integrity sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA== + dependencies: + "@types/hammerjs" "^2.0.45" + hammerjs "^2.0.8" + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -4700,6 +4730,11 @@ grid-index@^1.1.0: resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7" integrity sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA== +hammerjs@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1" + integrity sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ== + hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -6934,6 +6969,11 @@ rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: rc-resize-observer "^1.0.0" rc-util "^5.36.0" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" From 75ab43d91c19b1d86ba1772eebb95acd5ef87790 Mon Sep 17 00:00:00 2001 From: Graham Mather Date: Wed, 27 Nov 2024 11:58:54 -0500 Subject: [PATCH 2/4] Both charts fully implemented --- src/components/Admin/AdminData.tsx | 4 +- ...tOnboarding.tsx => BarChartUserCounts.tsx} | 61 ++++++++++++------- src/components/Admin/LineChartCount.tsx | 36 +++++++---- .../Profile/ControlledTimePicker.tsx | 2 +- src/server/router/user/admin.ts | 1 + src/utils/types.ts | 1 + 6 files changed, 67 insertions(+), 38 deletions(-) rename src/components/Admin/{BarChartOnboarding.tsx => BarChartUserCounts.tsx} (63%) diff --git a/src/components/Admin/AdminData.tsx b/src/components/Admin/AdminData.tsx index 05bd4757..6f872c23 100644 --- a/src/components/Admin/AdminData.tsx +++ b/src/components/Admin/AdminData.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Spinner from "../Spinner"; import { trpc } from "../../utils/trpc"; import { TempUser, TempGroup } from "../../utils/types"; -import BarChartOnboarding from "./BarChartOnboarding"; +import BarChartUserCounts from "./BarChartUserCounts"; import LineChartCount from "./LineChartCount"; function AdminData() { @@ -26,7 +26,7 @@ function AdminData() {
- +
diff --git a/src/components/Admin/BarChartOnboarding.tsx b/src/components/Admin/BarChartUserCounts.tsx similarity index 63% rename from src/components/Admin/BarChartOnboarding.tsx rename to src/components/Admin/BarChartUserCounts.tsx index 31cabdf7..3ae0195e 100644 --- a/src/components/Admin/BarChartOnboarding.tsx +++ b/src/components/Admin/BarChartUserCounts.tsx @@ -27,22 +27,47 @@ interface BarChartOnboardingProps { users: TempUser[]; } -function BarChartOnboarding({ users }: BarChartOnboardingProps) { +function BarChartUserCounts({ users }: BarChartOnboardingProps) { + const totalCount = users.length; const countOnboarded = users.filter((user) => user.isOnboarded).length; - const countNotOnboarded = users.length - countOnboarded; + const countNotOnboarded = totalCount - countOnboarded; + const driverCount = users.filter((user) => user.role === "DRIVER").length; + const riderCount = users.filter((user) => user.role === "RIDER").length; + + const viewerCount = totalCount - (driverCount + riderCount); + const dataPoints = [ + totalCount, + countOnboarded, + countNotOnboarded, + driverCount, + riderCount, + viewerCount, + ]; + const barColors = [ + "#000000", + "#FFA9A9", + "#808080", + "#C8102E", + "#DA7D25", + "#2454DD", + ]; + const labels = [ + "Total", + "Onboarded", + "Not Onboarded", + "Driver", + "Rider", + "Viewer", + ]; const barData: ChartData<"bar"> = { - labels: ["Users"], + labels, + datasets: [ { - label: "Onboarded", - data: [countOnboarded], - backgroundColor: "#C8102E", - }, - { - label: "Not Onboarded", - data: [countNotOnboarded], - backgroundColor: "#D3D3D3", + label: "User Counts", + data: dataPoints, + backgroundColor: barColors, }, ], }; @@ -52,19 +77,11 @@ function BarChartOnboarding({ users }: BarChartOnboardingProps) { maintainAspectRatio: false, plugins: { legend: { - position: "top", - labels: { - font: { - family: "Montserrat", - size: 16, - style: "normal", - weight: "bold", - }, - }, + display: false, }, title: { display: true, - text: "User Onboarding Status", + text: "User Counts", font: { family: "Montserrat", size: 18, @@ -110,4 +127,4 @@ function BarChartOnboarding({ users }: BarChartOnboardingProps) { ); } -export default BarChartOnboarding; +export default BarChartUserCounts; diff --git a/src/components/Admin/LineChartCount.tsx b/src/components/Admin/LineChartCount.tsx index 96af50d5..994ef8f0 100644 --- a/src/components/Admin/LineChartCount.tsx +++ b/src/components/Admin/LineChartCount.tsx @@ -15,7 +15,7 @@ import { } from "chart.js"; import "chartjs-adapter-date-fns"; import { format, startOfWeek, differenceInWeeks, addWeeks } from "date-fns"; -import { Slider } from "antd"; +import { Slider, ConfigProvider } from "antd"; ChartJS.register( CategoryScale, @@ -66,7 +66,7 @@ function LineChartCount({ users, groups }: LineChartCountProps) { setSliderRange(value); }; - // Filter items based on slider range + // Filter based on slider range const filteredUsers = filterItemsByDate( users, sliderRange[0], @@ -142,7 +142,7 @@ function LineChartCount({ users, groups }: LineChartCountProps) { labels: { font: { family: "Montserrat", - size: 18, + size: 16, style: "normal", weight: "bold", }, @@ -239,16 +239,26 @@ function LineChartCount({ users, groups }: LineChartCountProps) {
No data available for the selected date range.
)}
- -
+ + + +
{format(new Date(sliderRange[0]), "MMM dd, yyyy")} {format(new Date(sliderRange[1]), "MMM dd, yyyy")}
diff --git a/src/components/Profile/ControlledTimePicker.tsx b/src/components/Profile/ControlledTimePicker.tsx index a69ada77..db7bf56d 100644 --- a/src/components/Profile/ControlledTimePicker.tsx +++ b/src/components/Profile/ControlledTimePicker.tsx @@ -1,5 +1,5 @@ import { TimePicker, ConfigProvider } from "antd"; -import dayjs, { Dayjs } from "dayjs"; +import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; dayjs.extend(customParseFormat); import utc from "dayjs/plugin/utc"; diff --git a/src/server/router/user/admin.ts b/src/server/router/user/admin.ts index 9fb5c884..cd3b8b9b 100644 --- a/src/server/router/user/admin.ts +++ b/src/server/router/user/admin.ts @@ -18,6 +18,7 @@ export const adminDataRouter = router({ permission: true, isOnboarded: true, dateCreated: true, + role: true, }, }); }), diff --git a/src/utils/types.ts b/src/utils/types.ts index 48f9b66b..5cebb632 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -11,6 +11,7 @@ export type TempUser = { permission: Permission; isOnboarded: boolean; dateCreated: Date; + role: Role; }; export type TempGroup = { id: string; From a6e543263e2eec5efcddf1d54d76ddb5a79e7a90 Mon Sep 17 00:00:00 2001 From: Graham Mather Date: Wed, 27 Nov 2024 12:30:08 -0500 Subject: [PATCH 3/4] Slider step by week --- src/components/Admin/AdminSidebar.tsx | 3 +-- src/components/Admin/LineChartCount.tsx | 28 ++++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/Admin/AdminSidebar.tsx b/src/components/Admin/AdminSidebar.tsx index 211718a1..993dd9fc 100644 --- a/src/components/Admin/AdminSidebar.tsx +++ b/src/components/Admin/AdminSidebar.tsx @@ -4,8 +4,7 @@ type AdminSidebarProps = { }; const AdminSidebar = ({ option, setOption }: AdminSidebarProps) => { - const baseButton = - "px-4 py-2 text-northeastern-red font-montserrat text-base md:text-lg "; + const baseButton = "px-4 py-2 text-northeastern-red font-montserrat text-xl "; const selectedButton = " font-bold underline underline-offset-8 "; return (
diff --git a/src/components/Admin/LineChartCount.tsx b/src/components/Admin/LineChartCount.tsx index 994ef8f0..d3be8a2b 100644 --- a/src/components/Admin/LineChartCount.tsx +++ b/src/components/Admin/LineChartCount.tsx @@ -57,7 +57,10 @@ function LineChartCount({ users, groups }: LineChartCountProps) { setMinDate(minTimestamp); setMaxDate(maxTimestamp); - setSliderRange([minTimestamp, maxTimestamp]); + setSliderRange([ + startOfWeek(minTimestamp).getTime(), + startOfWeek(maxTimestamp).getTime(), + ]); } } }, [users, groups]); @@ -87,12 +90,10 @@ function LineChartCount({ users, groups }: LineChartCountProps) { let weekLabels: Date[] = []; if (allDates.length > 0) { const minWeekDate = startOfWeek( - new Date(Math.min(...allDates.map((date) => date.getTime()))), - { weekStartsOn: 1 } + new Date(Math.min(...allDates.map((date) => date.getTime()))) ); const maxWeekDate = startOfWeek( - new Date(Math.max(...allDates.map((date) => date.getTime()))), - { weekStartsOn: 1 } + new Date(Math.max(...allDates.map((date) => date.getTime()))) ); const weeksDifference = differenceInWeeks(maxWeekDate, minWeekDate) + 1; @@ -105,7 +106,6 @@ function LineChartCount({ users, groups }: LineChartCountProps) { const userCounts = countCumulativeItemsPerWeek(filteredUsers, weekLabels); const groupCounts = countCumulativeItemsPerWeek(filteredGroups, weekLabels); - const lineData: ChartData<"line"> = { labels: weekLabels, datasets: [ @@ -150,7 +150,7 @@ function LineChartCount({ users, groups }: LineChartCountProps) { }, title: { display: true, - text: "Cumulative Users and Groups Over Time", + text: "Cumulative Users and Current Groups Over Time", font: { family: "Montserrat", size: 18, @@ -250,17 +250,21 @@ function LineChartCount({ users, groups }: LineChartCountProps) { >
- {format(new Date(sliderRange[0]), "MMM dd, yyyy")} - {format(new Date(sliderRange[1]), "MMM dd, yyyy")} + + {format(startOfWeek(new Date(sliderRange[0])), "MMM dd, yyyy")} + + + {format(startOfWeek(new Date(sliderRange[1])), "MMM dd, yyyy")} +
From 20ad30711811a1402d1188588386a497263a43f0 Mon Sep 17 00:00:00 2001 From: Graham Mather Date: Wed, 27 Nov 2024 12:33:59 -0500 Subject: [PATCH 4/4] End range of slider inclusive --- src/utils/adminDataUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/adminDataUtils.ts b/src/utils/adminDataUtils.ts index b2461aff..b6e26536 100644 --- a/src/utils/adminDataUtils.ts +++ b/src/utils/adminDataUtils.ts @@ -1,4 +1,4 @@ -import { addWeeks } from "date-fns"; +import { addWeeks, startOfWeek } from "date-fns"; interface ItemWithDate { dateCreated: Date; @@ -9,7 +9,7 @@ export const filterItemsByDate = ( endTimestamp: number ) => { return items.filter((item) => { - const itemTimestamp = item.dateCreated.getTime(); + const itemTimestamp = startOfWeek(item.dateCreated).getTime(); return itemTimestamp >= startTimestamp && itemTimestamp <= endTimestamp; }); };