feat: Rearrange dashboard - pie chart + stats on top, spools middle, analytics bottom
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 23:32:03 -06:00
parent ad1df035e9
commit f38baa985e
3 changed files with 125 additions and 96 deletions

View File

@@ -20,7 +20,11 @@ const COLORS = [
"#2f54eb", // geekblue
];
export function MaterialDistributionChart() {
interface MaterialDistributionChartProps {
square?: boolean;
}
export function MaterialDistributionChart({ square }: MaterialDistributionChartProps) {
const t = useTranslate();
const { token } = useToken();
@@ -59,20 +63,21 @@ export function MaterialDistributionChart() {
<Card
title={t("home.charts.material_distribution")}
size="small"
style={{ marginBottom: 16 }}
style={square ? { aspectRatio: "1", display: "flex", flexDirection: "column" } : { marginBottom: 16 }}
styles={square ? { body: { flex: 1, padding: "8px 12px", display: "flex", flexDirection: "column" } } : undefined}
>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={square ? "100%" : 250}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
innerRadius={square ? 40 : 50}
outerRadius={square ? 70 : 80}
paddingAngle={2}
dataKey="value"
label={({ name, percent }) => `${name} (${((percent ?? 0) * 100).toFixed(0)}%)`}
labelLine={{ stroke: token.colorTextSecondary }}
label={square ? undefined : ({ name, percent }) => `${name} (${((percent ?? 0) * 100).toFixed(0)}%)`}
labelLine={square ? false : { stroke: token.colorTextSecondary }}
>
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
@@ -86,6 +91,13 @@ export function MaterialDistributionChart() {
borderRadius: token.borderRadius,
}}
/>
{square && (
<Legend
layout="horizontal"
verticalAlign="bottom"
wrapperStyle={{ fontSize: 11 }}
/>
)}
</PieChart>
</ResponsiveContainer>
</Card>

View File

@@ -12,7 +12,11 @@ import { getAPIURL } from "../../../utils/url";
const { useToken } = theme;
export function QuickStats() {
interface QuickStatsProps {
compact?: boolean;
}
export function QuickStats({ compact }: QuickStatsProps) {
const t = useTranslate();
const { token } = useToken();
const currencyFormatter = useCurrencyFormatter();
@@ -70,6 +74,67 @@ export function QuickStats() {
return `${Math.round(grams)} g`;
};
const stats = [
{
title: t("home.quick_stats.total_filaments"),
value: filamentsResult?.total ?? 0,
loading: filamentsQuery.isLoading,
prefix: <HighlightOutlined />,
to: "/filament",
},
{
title: t("home.quick_stats.total_vendors"),
value: vendorsResult?.total ?? 0,
loading: vendorsQuery.isLoading,
prefix: <UserOutlined />,
to: "/vendor",
},
{
title: t("home.quick_stats.total_weight"),
value: formatWeight(totalWeight),
loading: spoolsQuery.isLoading,
prefix: <DatabaseOutlined />,
},
{
title: t("home.quick_stats.total_value"),
value: currencyFormatter.format(totalValue),
loading: spoolsQuery.isLoading,
prefix: <DollarOutlined />,
},
{
title: t("home.quick_stats.total_spent"),
value: currencyFormatter.format(costStats?.total_spent ?? 0),
loading: costLoading,
prefix: <DollarOutlined />,
},
{
title: t("home.quick_stats.avg_cost_per_kg"),
value: costStats?.avg_cost_per_kg != null ? currencyFormatter.format(costStats.avg_cost_per_kg) : "-",
loading: costLoading,
prefix: <RiseOutlined />,
},
];
if (compact) {
return (
<Row gutter={[8, 8]}>
{stats.map((stat, i) => (
<Col xs={12} key={i}>
<Card size="small" styles={{ body: { padding: "8px 12px" } }}>
<Statistic
title={stat.title}
value={stat.value}
loading={stat.loading}
prefix={stat.prefix}
valueStyle={{ fontSize: 16 }}
/>
</Card>
</Col>
))}
</Row>
);
}
return (
<Row gutter={[12, 12]} style={{ marginTop: 16 }}>
<Col xs={12} md={8} lg={4}>
@@ -84,70 +149,21 @@ export function QuickStats() {
</Card>
</Link>
</Col>
<Col xs={12} md={8} lg={4}>
<Link to="/filament">
<Card hoverable size="small">
<Statistic
title={t("home.quick_stats.total_filaments")}
value={filamentsResult?.total ?? 0}
loading={filamentsQuery.isLoading}
prefix={<HighlightOutlined />}
/>
</Card>
</Link>
</Col>
<Col xs={12} md={8} lg={4}>
<Link to="/vendor">
<Card hoverable size="small">
<Statistic
title={t("home.quick_stats.total_vendors")}
value={vendorsResult?.total ?? 0}
loading={vendorsQuery.isLoading}
prefix={<UserOutlined />}
/>
</Card>
</Link>
</Col>
<Col xs={12} md={12} lg={6}>
<Card size="small">
<Statistic
title={t("home.quick_stats.total_weight")}
value={formatWeight(totalWeight)}
loading={spoolsQuery.isLoading}
prefix={<DatabaseOutlined />}
/>
</Card>
</Col>
<Col xs={12} md={12} lg={6}>
<Card size="small">
<Statistic
title={t("home.quick_stats.total_value")}
value={currencyFormatter.format(totalValue)}
loading={spoolsQuery.isLoading}
prefix={<DollarOutlined />}
/>
</Card>
</Col>
<Col xs={12} md={12} lg={6}>
<Card size="small">
<Statistic
title={t("home.quick_stats.total_spent")}
value={currencyFormatter.format(costStats?.total_spent ?? 0)}
loading={costLoading}
prefix={<DollarOutlined />}
/>
</Card>
</Col>
<Col xs={12} md={12} lg={6}>
<Card size="small">
<Statistic
title={t("home.quick_stats.avg_cost_per_kg")}
value={costStats?.avg_cost_per_kg != null ? currencyFormatter.format(costStats.avg_cost_per_kg) : "-"}
loading={costLoading}
prefix={<RiseOutlined />}
/>
</Card>
</Col>
{stats.map((stat, i) => (
<Col xs={12} md={8} lg={4} key={i}>
{stat.to ? (
<Link to={stat.to}>
<Card hoverable size="small">
<Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
</Card>
</Link>
) : (
<Card size="small">
<Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
</Card>
)}
</Col>
))}
</Row>
);
}

View File

@@ -112,37 +112,38 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
fontFamily: token.fontFamily,
}}
>
{/* Header with spool count and add button */}
<Row justify="space-between" align="middle" style={{ marginBottom: 16 }}>
<Col>
<Statistic
title={t("spool.spool")}
value={spoolsResult?.total ?? 0}
loading={spoolsQuery.isLoading}
valueStyle={{ fontSize: 36, fontWeight: 600 }}
/>
{/* Top section: Pie chart + stats/alerts side by side */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col xs={24} md={10}>
<MaterialDistributionChart square />
</Col>
<Col>
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/spool/create")}>
{t("spool.titles.create")}
</Button>
<Col xs={24} md={14}>
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
<Col>
<Statistic
title={t("spool.spool")}
value={spoolsResult?.total ?? 0}
loading={spoolsQuery.isLoading}
valueStyle={{ fontSize: 36, fontWeight: 600 }}
/>
</Col>
<Col>
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/spool/create")}>
{t("spool.titles.create")}
</Button>
</Col>
</Row>
<AlertCards />
<QuickStats compact />
</Col>
</Row>
{/* Alert badges */}
<AlertCards />
{/* Material distribution chart */}
<MaterialDistributionChart />
{/* Usage analytics */}
<UsageAnalytics />
{/* Spool list */}
<Card
title={t("home.all_spools")}
extra={<Link to="/spool">{t("buttons.viewAll")}</Link>}
styles={{ body: { padding: 0 } }}
style={{ marginBottom: 16 }}
>
<Table<ISpool>
dataSource={spoolsResult?.data as ISpool[]}
@@ -168,8 +169,8 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
</div>
</Card>
{/* Quick stats */}
<QuickStats />
{/* Usage analytics & insights */}
<UsageAnalytics />
</Content>
);
};