Add user purchase statistics tab

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
leanderandCopilot committed 2026-10-22 05:39:45 +02:00
1 parent 4848dda4a4
commit 78d5b06258
6 files changed
+74 -5

No files matched your search

-4
View File
@@ -34,10 +34,6 @@ To create the admin during data seeding instead, set `BBS_SEED_ADMIN_PASSWORD` f
- We use JS code generation to work with queries/mutations in our graphql API: <https://github.com/dotansimha/graphql-typed-document-node>
## TODO
- Stats: Implement users tab
## TODO-old
- Larger pictures
+2
View File
@@ -12,6 +12,7 @@ const Register = React.lazy(() => import("./features/register/Register"));
const OrderStats = React.lazy(() => import("./features/stats/OrderStats"));
const ProductStats = React.lazy(() => import("./features/stats/ProductStats"));
const Stats = React.lazy(() => import("./features/stats/Stats"));
const UserStats = React.lazy(() => import("./features/stats/UserStats"));
const UserDetail = React.lazy(() => import("./features/users/UserDetail"));
const UserList = React.lazy(() => import("./features/users/UserList"));
@@ -26,6 +27,7 @@ export const Routes = (): JSX.Element => (
<Route path="/stats" element={<Stats />} />
<Route path="/stats/orders" element={<OrderStats />} />
<Route path="/stats/products" element={<ProductStats />} />
<Route path="/stats/users" element={<UserStats />} />
<Route path="/users" element={<UserList />} />
<Route path="/users/create" element={<UserCreate />} />
<Route path="/user/:userId" element={<UserDetail />} />
+1 -1
View File
@@ -25,7 +25,7 @@ export const StatsNav = () => (
</NavLink>
</li>
<li>
<NavLink className="disabled" to="/stats/users" title="TODO" hidden={true}>
<NavLink className={navLinkStyles} to="/stats/users">
Folk
</NavLink>
</li>
+55
View File
@@ -0,0 +1,55 @@
import { useQuery } from "@apollo/client";
import { ResponsiveBar } from "@nivo/bar";
import React from "react";
import { GQLError } from "../../components/GQLError";
import { UserStatsDocument } from "../../types";
import styles from "./Stats.module.css";
import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme";
const UserStats = () => {
const { data, loading, error } = useQuery(UserStatsDocument);
if (error) return <GQLError error={error} />;
const users = (data?.allUsers || [])
.filter((user) => Number(user.profile?.orderSumTotal || 0) > 0)
.map((user) => ({
name: `${user.firstName}${user.lastName ? ` ${user.lastName[0]}.` : ""}`,
total: Number(user.profile?.orderSumTotal || 0),
}))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
return (
<StatsView loading={loading}>
<h3>Topp 10 etter total kjøpesum</h3>
{users.length ? (
<div className={styles.chart} style={{ height: Math.max(320, users.length * 44) }}>
<ResponsiveBar
data={users}
keys={["total"]}
indexBy="name"
layout="horizontal"
theme={chartTheme}
margin={{ top: 10, right: 35, bottom: 40, left: 100 }}
padding={0.3}
enableLabel={false}
axisBottom={{
legend: "Kjøp totalt (kr)",
legendPosition: "middle",
legendOffset: 32,
}}
axisLeft={null}
valueFormat={(value) => `${Number(value).toLocaleString("no")} kr`}
/>
</div>
) : (
<p>Ingen kjøp å vise ennå.</p>
)}
</StatsView>
);
};
export default UserStats;
@@ -0,0 +1,10 @@
query userStats {
allUsers {
id
firstName
lastName
profile {
orderSumTotal
}
}
}
+6
View File
@@ -452,6 +452,11 @@ export type ProductStatsQueryVariables = Exact<{ [key: string]: never; }>;
export type ProductStatsQuery = { __typename: 'Query', productStats: Array<{ __typename: 'ProductOrderStat', productName: string, data: Array<{ __typename: 'Point', x: string, y: number }> }> };
export type UserStatsQueryVariables = Exact<{ [key: string]: never; }>;
export type UserStatsQuery = { __typename: 'Query', allUsers: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, profile: { __typename: 'UserProfile', orderSumTotal: string } | null }> };
export type UserDetailQueryVariables = Exact<{
userId: string | number;
}>;
@@ -474,4 +479,5 @@ export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"Op
export const AllProductStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProductStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userCounts"}}]}}]}}]} as unknown as DocumentNode<AllProductStatsQuery, AllProductStatsQueryVariables>;
export const OrderStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"orderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"daily"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"hourly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode<OrderStatsQuery, OrderStatsQueryVariables>;
export const ProductStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"productStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}}]}}]} as unknown as DocumentNode<ProductStatsQuery, ProductStatsQueryVariables>;
export const UserStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"userStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orderSumTotal"}}]}}]}}]}}]} as unknown as DocumentNode<UserStatsQuery, UserStatsQueryVariables>;
export const UserDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"userDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}},{"kind":"Field","name":{"kind":"Name","value":"topMonths"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"sum"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"productOrderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSumTotal"}}]}}]}}]}}]} as unknown as DocumentNode<UserDetailQuery, UserDetailQueryVariables>;