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

View File

@@ -12,7 +12,11 @@ import { getAPIURL } from "../../../utils/url";
const { useToken } = theme; const { useToken } = theme;
export function QuickStats() { interface QuickStatsProps {
compact?: boolean;
}
export function QuickStats({ compact }: QuickStatsProps) {
const t = useTranslate(); const t = useTranslate();
const { token } = useToken(); const { token } = useToken();
const currencyFormatter = useCurrencyFormatter(); const currencyFormatter = useCurrencyFormatter();
@@ -70,6 +74,67 @@ export function QuickStats() {
return `${Math.round(grams)} g`; 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 ( return (
<Row gutter={[12, 12]} style={{ marginTop: 16 }}> <Row gutter={[12, 12]} style={{ marginTop: 16 }}>
<Col xs={12} md={8} lg={4}> <Col xs={12} md={8} lg={4}>
@@ -84,70 +149,21 @@ export function QuickStats() {
</Card> </Card>
</Link> </Link>
</Col> </Col>
<Col xs={12} md={8} lg={4}> {stats.map((stat, i) => (
<Link to="/filament"> <Col xs={12} md={8} lg={4} key={i}>
{stat.to ? (
<Link to={stat.to}>
<Card hoverable size="small"> <Card hoverable size="small">
<Statistic <Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
title={t("home.quick_stats.total_filaments")}
value={filamentsResult?.total ?? 0}
loading={filamentsQuery.isLoading}
prefix={<HighlightOutlined />}
/>
</Card> </Card>
</Link> </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"> <Card size="small">
<Statistic <Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
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> </Card>
)}
</Col> </Col>
))}
</Row> </Row>
); );
} }

View File

@@ -112,8 +112,13 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
fontFamily: token.fontFamily, fontFamily: token.fontFamily,
}} }}
> >
{/* Header with spool count and add button */} {/* Top section: Pie chart + stats/alerts side by side */}
<Row justify="space-between" align="middle" style={{ marginBottom: 16 }}> <Row gutter={16} style={{ marginBottom: 16 }}>
<Col xs={24} md={10}>
<MaterialDistributionChart square />
</Col>
<Col xs={24} md={14}>
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
<Col> <Col>
<Statistic <Statistic
title={t("spool.spool")} title={t("spool.spool")}
@@ -128,21 +133,17 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
</Button> </Button>
</Col> </Col>
</Row> </Row>
{/* Alert badges */}
<AlertCards /> <AlertCards />
<QuickStats compact />
{/* Material distribution chart */} </Col>
<MaterialDistributionChart /> </Row>
{/* Usage analytics */}
<UsageAnalytics />
{/* Spool list */} {/* Spool list */}
<Card <Card
title={t("home.all_spools")} title={t("home.all_spools")}
extra={<Link to="/spool">{t("buttons.viewAll")}</Link>} extra={<Link to="/spool">{t("buttons.viewAll")}</Link>}
styles={{ body: { padding: 0 } }} styles={{ body: { padding: 0 } }}
style={{ marginBottom: 16 }}
> >
<Table<ISpool> <Table<ISpool>
dataSource={spoolsResult?.data as ISpool[]} dataSource={spoolsResult?.data as ISpool[]}
@@ -168,8 +169,8 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
</div> </div>
</Card> </Card>
{/* Quick stats */} {/* Usage analytics & insights */}
<QuickStats /> <UsageAnalytics />
</Content> </Content>
); );
}; };