From 0a741c9712c0d3aa20843cb53f2ced0ebc445662 Mon Sep 17 00:00:00 2001 From: tonym Date: Thu, 15 Jan 2026 23:27:14 -0600 Subject: [PATCH] feat(home): redesign dashboard with spool list and low stock alerts Replaces stat card layout with functional dashboard: - Prominent spool count with "Add Spool" button - Paginated list of all spools sorted by last used - Low stock alert badge (< 100g threshold) - Color swatch, name, material, and remaining weight columns - Clickable rows navigate to spool detail - Secondary stats for filaments and vendors Closes #12 Co-Authored-By: Claude Opus 4.5 --- client/public/locales/en/common.json | 8 +- client/src/pages/home/index.tsx | 292 +++++++++++++++++++-------- 2 files changed, 211 insertions(+), 89 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 992a65f..510edff 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -33,7 +33,8 @@ "no": "No", "notAccessTitle": "You don't have permission to access", "hideColumns": "Hide Columns", - "clearFilters": "Clear Filters" + "clearFilters": "Clear Filters", + "viewAll": "View All" }, "warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.", "notifications": { @@ -324,7 +325,10 @@ "home": { "home": "Home", "welcome": "Welcome to your Spoolman instance!", - "description": "It looks like you haven't added any spools yet. See the Help page for help getting started." + "description": "It looks like you haven't added any spools yet. See the Help page for help getting started.", + "all_spools": "All Spools", + "low_stock_alert": "{{count}} spools below {{threshold}}g", + "low_stock_warning": "Low stock" }, "help": { "help": "Help", diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index 525e219..3b8cee4 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -1,129 +1,247 @@ -import { FileOutlined, HighlightOutlined, PlusOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons"; +import { + ExclamationCircleOutlined, + HighlightOutlined, + PlusOutlined, + UserOutlined, +} from "@ant-design/icons"; import { IResourceComponentsProps, useList, useTranslate } from "@refinedev/core"; -import { Card, Col, Row, Statistic, theme } from "antd"; +import { Badge, Button, Card, Col, Pagination, Row, Statistic, Table, theme, Tooltip } from "antd"; import { Content } from "antd/es/layout/layout"; -import Title from "antd/es/typography/Title"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; -import React, { ReactNode } from "react"; -import { Trans } from "react-i18next"; -import { Link } from "react-router"; -import Logo from "../../icon.svg?react"; +import React, { useMemo, useState } from "react"; +import { Link, useNavigate } from "react-router"; +import SpoolIcon from "../../components/spoolIcon"; +import { IFilament } from "../filaments/model"; import { ISpool } from "../spools/model"; +import { IVendor } from "../vendors/model"; dayjs.extend(utc); const { useToken } = theme; +const LOW_STOCK_THRESHOLD = 100; // grams + export const Home: React.FC = () => { const { token } = useToken(); const t = useTranslate(); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 15; + + // Fetch all spools (not archived) for the main list const spools = useList({ resource: "spool", - pagination: { pageSize: 1 }, + pagination: { current: currentPage, pageSize }, + sorters: [{ field: "last_used", order: "desc" }], + meta: { + queryParams: { + allow_archived: false, + }, + }, }); - const filaments = useList({ + + // Fetch counts for stats + const filaments = useList({ resource: "filament", pagination: { pageSize: 1 }, }); - const vendors = useList({ + const vendors = useList({ resource: "vendor", pagination: { pageSize: 1 }, }); - const hasSpools = !spools.result || spools.result.data.length > 0; + // Calculate low stock spools + const lowStockCount = useMemo(() => { + if (!spools.data?.data) return 0; + return spools.data.data.filter( + (spool) => spool.remaining_weight !== undefined && spool.remaining_weight < LOW_STOCK_THRESHOLD + ).length; + }, [spools.data?.data]); - const ResourceStatsCard = (props: { loading: boolean; value: number; resource: string; icon: ReactNode }) => ( - - e.stopPropagation()}> - - , - e.stopPropagation()}> - - , - ]} - > - - - - - - ); + // For accurate low stock count, we need all spools + const allSpoolsForLowStock = useList({ + resource: "spool", + pagination: { pageSize: 1000 }, + filters: [ + { + field: "remaining_weight", + operator: "lt", + value: LOW_STOCK_THRESHOLD, + }, + ], + meta: { + queryParams: { + allow_archived: false, + }, + }, + }); + + const totalLowStock = allSpoolsForLowStock.data?.total ?? lowStockCount; + + const spoolColumns = [ + { + title: "", + dataIndex: "color", + key: "color", + width: 50, + render: (_: unknown, record: ISpool) => { + const colorObj = record.filament.multi_color_hexes + ? { + colors: record.filament.multi_color_hexes.split(","), + vertical: record.filament.multi_color_direction === "longitudinal", + } + : record.filament.color_hex; + return colorObj ? : null; + }, + }, + { + title: t("spool.fields.filament_name"), + dataIndex: "filament", + key: "filament", + render: (_: unknown, record: ISpool) => { + const vendorName = record.filament.vendor?.name; + const filamentName = record.filament.name ?? `Filament #${record.filament.id}`; + return vendorName ? `${vendorName} - ${filamentName}` : filamentName; + }, + }, + { + title: t("spool.fields.material"), + dataIndex: ["filament", "material"], + key: "material", + width: 100, + }, + { + title: t("spool.fields.remaining_weight"), + dataIndex: "remaining_weight", + key: "remaining_weight", + width: 120, + align: "right" as const, + render: (weight: number | undefined, record: ISpool) => { + if (weight === undefined) return t("unknown"); + const isLowStock = weight < LOW_STOCK_THRESHOLD; + return ( + + {Math.round(weight)} g + {isLowStock && ( + + + + )} + + ); + }, + }, + ]; return ( - - <div - style={{ - display: "inline-block", - height: "1.5em", - marginRight: "0.5em", - }} - > - <Logo /> - </div> - Spoolman - - - } - /> - } - /> - } - /> + {/* Header with spool count and add button */} + + + + + + + - {!hasSpools && ( - <> -

{t("home.welcome")}

-

- , - }} - /> -

- + + {/* Alert badges */} + {totalLowStock > 0 && ( + + + + + + + {t("home.low_stock_alert", { count: totalLowStock, threshold: LOW_STOCK_THRESHOLD })} + + + + + )} + + {/* Spool list */} + {t("buttons.viewAll")}} + styles={{ body: { padding: 0 } }} + > + ({ + onClick: () => navigate(`/spool/show/${record.id}`), + style: { cursor: "pointer" }, + })} + /> +
+ +
+ + + {/* Secondary stats */} + + + + + } + /> + + + + + + + } + /> + + + + ); };