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
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:
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
<Card hoverable size="small">
|
{stat.to ? (
|
||||||
<Statistic
|
<Link to={stat.to}>
|
||||||
title={t("home.quick_stats.total_filaments")}
|
<Card hoverable size="small">
|
||||||
value={filamentsResult?.total ?? 0}
|
<Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
|
||||||
loading={filamentsQuery.isLoading}
|
</Card>
|
||||||
prefix={<HighlightOutlined />}
|
</Link>
|
||||||
/>
|
) : (
|
||||||
</Card>
|
<Card size="small">
|
||||||
</Link>
|
<Statistic title={stat.title} value={stat.value} loading={stat.loading} prefix={stat.prefix} />
|
||||||
</Col>
|
</Card>
|
||||||
<Col xs={12} md={8} lg={4}>
|
)}
|
||||||
<Link to="/vendor">
|
</Col>
|
||||||
<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>
|
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,37 +112,38 @@ 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>
|
<Col xs={24} md={10}>
|
||||||
<Statistic
|
<MaterialDistributionChart square />
|
||||||
title={t("spool.spool")}
|
|
||||||
value={spoolsResult?.total ?? 0}
|
|
||||||
loading={spoolsQuery.isLoading}
|
|
||||||
valueStyle={{ fontSize: 36, fontWeight: 600 }}
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col xs={24} md={14}>
|
||||||
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/spool/create")}>
|
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||||
{t("spool.titles.create")}
|
<Col>
|
||||||
</Button>
|
<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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Alert badges */}
|
|
||||||
<AlertCards />
|
|
||||||
|
|
||||||
{/* Material distribution chart */}
|
|
||||||
<MaterialDistributionChart />
|
|
||||||
|
|
||||||
{/* 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user