✨ Split out stats into multiple sub views
This commit is contained in:
1 parent
158f5c5652
commit
2e5fbb6fa6
7 files changed
+171
-109
No files matched your search
@@ -23,7 +23,7 @@ npm start
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- update emails
|
- update emails
|
||||||
- Stats: subnav, products over time grouped by month compared to last month, move per product stats to subview
|
- Stats: products over time
|
||||||
- Light on dark colors
|
- Light on dark colors
|
||||||
- inventory views
|
- inventory views
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { UserCreate } from "./features/auth/UserCreate";
|
|||||||
import { Deposit } from "./features/deposit/Deposit";
|
import { Deposit } from "./features/deposit/Deposit";
|
||||||
import { Log } from "./features/log/Log";
|
import { Log } from "./features/log/Log";
|
||||||
import { Register } from "./features/register/Register";
|
import { Register } from "./features/register/Register";
|
||||||
|
import { OrderStats } from "./features/stats/OrderStats";
|
||||||
|
import { ProductStats } from "./features/stats/ProductStats";
|
||||||
import { Stats } from "./features/stats/Stats";
|
import { Stats } from "./features/stats/Stats";
|
||||||
import { UserDetail } from "./features/users/UserDetail";
|
import { UserDetail } from "./features/users/UserDetail";
|
||||||
import { UserList } from "./features/users/UserList";
|
import { UserList } from "./features/users/UserList";
|
||||||
@@ -14,6 +16,8 @@ export const Routes: React.FC = () => (
|
|||||||
<Route path="/deposit" element={<Deposit />} />
|
<Route path="/deposit" element={<Deposit />} />
|
||||||
<Route path="/log" element={<Log />} />
|
<Route path="/log" element={<Log />} />
|
||||||
<Route path="/stats" element={<Stats />} />
|
<Route path="/stats" element={<Stats />} />
|
||||||
|
<Route path="/stats/orders" element={<OrderStats />} />
|
||||||
|
<Route path="/stats/products" element={<ProductStats />} />
|
||||||
<Route path="/users" element={<UserList />} />
|
<Route path="/users" element={<UserList />} />
|
||||||
<Route path="/user/create" element={<UserCreate />} />
|
<Route path="/user/create" element={<UserCreate />} />
|
||||||
<Route path="/user/:userId" element={<UserDetail />} />
|
<Route path="/user/:userId" element={<UserDetail />} />
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Bar, BarDatum } from "@nivo/bar";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { OrderStatsDocument, OrderStatsByTime } from "../../types";
|
||||||
|
import { StatsView } from "./StatsView";
|
||||||
|
|
||||||
|
const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" };
|
||||||
|
|
||||||
|
const months = {
|
||||||
|
1: "Januar",
|
||||||
|
2: "Februar",
|
||||||
|
3: "Mars",
|
||||||
|
4: "April",
|
||||||
|
5: "Mai",
|
||||||
|
6: "Juni",
|
||||||
|
7: "Juli",
|
||||||
|
8: "August",
|
||||||
|
9: "September",
|
||||||
|
10: "Oktober",
|
||||||
|
11: "November",
|
||||||
|
12: "Desember",
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderBar = ({ data }: { data: BarDatum[] }) => (
|
||||||
|
<Bar
|
||||||
|
data={data}
|
||||||
|
margin={{ left: 60, bottom: 40 }}
|
||||||
|
width={1200}
|
||||||
|
height={400}
|
||||||
|
colorBy="indexValue"
|
||||||
|
enableLabel={false}
|
||||||
|
tooltipLabel={(d) => d.id.toString()}
|
||||||
|
axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OrderStats = () => {
|
||||||
|
const { data, loading } = useQuery(OrderStatsDocument);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsView loading={loading}>
|
||||||
|
<h3>Ordre per time</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.hourly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
id: `${stat.period.toString().padStart(2, "0")}:00`,
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per ukedag</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.daily || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
/* @ts-expect-error Here, stat.period can only be a key in days range. */
|
||||||
|
id: days[stat.period],
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per måned</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.monthly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
/* @ts-expect-error Here, stat.period can only be a key in months range. */
|
||||||
|
id: months[stat.period],
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per år</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.yearly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
id: stat.period,
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</StatsView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Pie } from "@nivo/pie";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ProductStatsDocument } from "../../types";
|
||||||
|
import styles from "./Stats.module.css";
|
||||||
|
import { StatsView } from "./StatsView";
|
||||||
|
|
||||||
|
type UserCount = {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductStats = () => {
|
||||||
|
const { data, loading } = useQuery(ProductStatsDocument, {
|
||||||
|
variables: { active: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsView loading={loading}>
|
||||||
|
<div className={styles.products}>
|
||||||
|
{(data?.allProducts || []).map((product) => (
|
||||||
|
<div key={product.id}>
|
||||||
|
{product.name}
|
||||||
|
<Pie
|
||||||
|
margin={{ top: 40, right: 120, bottom: 40, left: 120 }}
|
||||||
|
data={product.userCounts.map((uc: UserCount) => ({
|
||||||
|
id: uc.name,
|
||||||
|
label: uc.name,
|
||||||
|
value: uc.count,
|
||||||
|
}))}
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</StatsView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,3 +4,6 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
.statsNav {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
@@ -1,113 +1,8 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import { Bar, BarDatum } from "@nivo/bar";
|
|
||||||
import { Pie } from "@nivo/pie";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { OrderStatsDocument, ProductStatsDocument, OrderStatsByTime } from "../../types";
|
import { StatsView } from "./StatsView";
|
||||||
import styles from "./Stats.module.css";
|
|
||||||
|
|
||||||
type UserCount = {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" };
|
|
||||||
|
|
||||||
const months = {
|
|
||||||
1: "Januar",
|
|
||||||
2: "Februar",
|
|
||||||
3: "Mars",
|
|
||||||
4: "April",
|
|
||||||
5: "Mai",
|
|
||||||
6: "Juni",
|
|
||||||
7: "Juli",
|
|
||||||
8: "August",
|
|
||||||
9: "September",
|
|
||||||
10: "Oktober",
|
|
||||||
11: "November",
|
|
||||||
12: "Desember",
|
|
||||||
};
|
|
||||||
|
|
||||||
const OrderBar = ({ data }: { data: BarDatum[] }) => (
|
|
||||||
<Bar
|
|
||||||
data={data}
|
|
||||||
margin={{ left: 60, bottom: 40 }}
|
|
||||||
width={1200}
|
|
||||||
height={400}
|
|
||||||
colorBy="indexValue"
|
|
||||||
enableLabel={false}
|
|
||||||
tooltipLabel={(d) => d.id.toString()}
|
|
||||||
axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Stats = () => {
|
export const Stats = () => {
|
||||||
const { data: productData, loading: productLoading } = useQuery(ProductStatsDocument, {
|
const loading = false;
|
||||||
variables: { active: true },
|
|
||||||
});
|
|
||||||
const { data: orderData, loading: orderLoading } = useQuery(OrderStatsDocument);
|
|
||||||
|
|
||||||
const loading = orderLoading || productLoading;
|
return <StatsView loading={loading}>TODO</StatsView>;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.stats}>
|
|
||||||
<h2>Stats</h2>
|
|
||||||
<h3>Produkter</h3>
|
|
||||||
<div className={styles.products}>
|
|
||||||
{(productData?.allProducts || []).map((product) => (
|
|
||||||
<div key={product.id}>
|
|
||||||
{product.name}
|
|
||||||
<Pie
|
|
||||||
margin={{ top: 40, right: 120, bottom: 40, left: 120 }}
|
|
||||||
data={product.userCounts.map((uc: UserCount) => ({
|
|
||||||
id: uc.name,
|
|
||||||
label: uc.name,
|
|
||||||
value: uc.count,
|
|
||||||
}))}
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<h3>Ordre per time</h3>
|
|
||||||
<OrderBar
|
|
||||||
data={(orderData?.orderStats?.hourly || []).map((stat: OrderStatsByTime) => ({
|
|
||||||
id: `${stat.period.toString().padStart(2, "0")}:00`,
|
|
||||||
label: stat.period,
|
|
||||||
value: stat.count,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<h3>Ordre per ukedag</h3>
|
|
||||||
<OrderBar
|
|
||||||
data={(orderData?.orderStats?.daily || []).map((stat: OrderStatsByTime) => ({
|
|
||||||
/* @ts-expect-error Here, stat.period can only be a key in days range. */
|
|
||||||
id: days[stat.period],
|
|
||||||
label: stat.period,
|
|
||||||
value: stat.count,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<h3>Ordre per måned</h3>
|
|
||||||
<OrderBar
|
|
||||||
data={(orderData?.orderStats?.monthly || []).map((stat: OrderStatsByTime) => ({
|
|
||||||
/* @ts-expect-error Here, stat.period can only be a key in months range. */
|
|
||||||
id: months[stat.period],
|
|
||||||
label: stat.period,
|
|
||||||
value: stat.count,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<h3>Ordre per år</h3>
|
|
||||||
<OrderBar
|
|
||||||
data={(orderData?.orderStats?.yearly || []).map((stat: OrderStatsByTime) => ({
|
|
||||||
id: stat.period,
|
|
||||||
label: stat.period,
|
|
||||||
value: stat.count,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import styles from "./Stats.module.css";
|
||||||
|
const navLinkStyles = ({ isActive }: { isActive: boolean }) => (isActive ? "nav-link active" : "nav-link");
|
||||||
|
|
||||||
|
export const StatsNav = () => (
|
||||||
|
<div className={styles.statsNav}>
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats" end>
|
||||||
|
Oversikt
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats/products">
|
||||||
|
Produkter
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats/orders">
|
||||||
|
Ordre
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className="nav-link disabled" to="/stats/users" title="TODO">
|
||||||
|
Folk
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StatsView = ({ children, loading }: { children: React.ReactNode; loading: boolean }) => (
|
||||||
|
<div className={styles.stats}>
|
||||||
|
<h2>Statistikk</h2>
|
||||||
|
<StatsNav />
|
||||||
|
{loading && <div>Loading...</div>}
|
||||||
|
{!loading && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
Reference in new issue
Block a user