From d38af61ba2fb3c87143da57ddd92bc4845f85a7c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 1 Jun 2026 21:44:25 +1000 Subject: [PATCH] [UI] Order history dashboard widgets (#12061) * Add order history dashboard widgets * Support icons for dashboard widgets * Change StylishText size from 'lg' to 'md' --- .../components/dashboard/DashboardWidget.tsx | 2 + .../dashboard/DashboardWidgetDrawer.tsx | 5 +- .../dashboard/DashboardWidgetLibrary.tsx | 52 +++++-- .../dashboard/widgets/OrderHistoryWidget.tsx | 131 ++++++++++++++++++ .../widgets/QueryCountDashboardWidget.tsx | 3 + .../widgets/StocktakeDashboardWidget.tsx | 1 + src/frontend/src/functions/icons.tsx | 11 +- 7 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx diff --git a/src/frontend/src/components/dashboard/DashboardWidget.tsx b/src/frontend/src/components/dashboard/DashboardWidget.tsx index cc806d01b9..b6809bcc08 100644 --- a/src/frontend/src/components/dashboard/DashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidget.tsx @@ -5,6 +5,7 @@ import { IconX } from '@tabler/icons-react'; import { Boundary } from '@lib/components/Boundary'; import type { ModelType } from '@lib/index'; +import type { InvenTreeIconType } from '@lib/types/Icons'; import type { JSX } from 'react'; /** @@ -22,6 +23,7 @@ export interface DashboardWidgetProps { minWidth?: number; minHeight?: number; modelType?: ModelType; + icon?: keyof InvenTreeIconType; render: () => JSX.Element; visible?: () => boolean; } diff --git a/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx index 397111102e..b4ad089471 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx @@ -12,10 +12,11 @@ import { Tooltip } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; -import { IconBackspace, IconLayoutGridAdd } from '@tabler/icons-react'; +import { IconBackspace } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; import { StylishText } from '@lib/components/StylishText'; +import { InvenTreeIcon } from '../../functions/icons'; import { useDashboardItems } from '../../hooks/UseDashboardItems'; /** @@ -105,7 +106,7 @@ export default function DashboardWidgetDrawer({ onAddWidget(widget.label); }} > - + diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index 5ef7b5f04f..76ada27f8d 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -8,6 +8,7 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import GetStartedWidget from './widgets/GetStartedWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import NewsWidget from './widgets/NewsWidget'; +import OrderHistoryWidget from './widgets/OrderHistoryWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; import QueryDashboardWidget from './widgets/QueryDashboardWidget'; import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget'; @@ -43,6 +44,7 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { title: t`Invalid BOMs`, description: t`Assemblies requiring bill of materials validation`, modelType: ModelType.part, + icon: 'exclamation', params: { active: true, // Only show active parts assembly: true, // Only show parts which are assemblies @@ -94,7 +96,8 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { description: t`Show the number of stock items which have expired`, modelType: ModelType.stockitem, params: { expired: true }, - enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY') + enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY'), + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Stale Stock Items`, @@ -116,14 +119,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-bo', description: t`Show the number of build orders which are overdue`, modelType: ModelType.build, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Build Orders`, label: 'asn-bo', description: t`Show the number of build orders which are assigned to you`, modelType: ModelType.build, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Active Sales Orders`, @@ -137,14 +142,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-so', description: t`Show the number of sales orders which are overdue`, modelType: ModelType.salesorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Sales Orders`, label: 'asn-so', description: t`Show the number of sales orders which are assigned to you`, modelType: ModelType.salesorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Pending Shipments`, @@ -165,14 +172,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-po', description: t`Show the number of purchase orders which are overdue`, modelType: ModelType.purchaseorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Purchase Orders`, label: 'asn-po', description: t`Show the number of purchase orders which are assigned to you`, modelType: ModelType.purchaseorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }), QueryCountDashboardWidget({ title: t`Active Return Orders`, @@ -186,14 +195,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { label: 'ovr-ro', description: t`Show the number of return orders which are overdue`, modelType: ModelType.returnorder, - params: { overdue: true } + params: { overdue: true }, + icon: 'overdue' }), QueryCountDashboardWidget({ title: t`Assigned Return Orders`, label: 'asn-ro', description: t`Show the number of return orders which are assigned to you`, modelType: ModelType.returnorder, - params: { assigned_to_me: true, outstanding: true } + params: { assigned_to_me: true, outstanding: true }, + icon: 'responsible' }) ]; @@ -207,6 +218,26 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { }); } +function BuiltinHistoryWidgets(): DashboardWidgetProps[] { + return [ + OrderHistoryWidget({ + modelType: ModelType.build + }), + OrderHistoryWidget({ + modelType: ModelType.salesorder + }), + OrderHistoryWidget({ + modelType: ModelType.purchaseorder + }), + OrderHistoryWidget({ + modelType: ModelType.returnorder + }), + OrderHistoryWidget({ + modelType: ModelType.transferorder + }) + ]; +} + function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { return [ { @@ -215,6 +246,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { description: t`Getting started with InvenTree`, minWidth: 5, minHeight: 4, + icon: 'info', render: () => }, { @@ -223,6 +255,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { description: t`The latest news from InvenTree`, minWidth: 5, minHeight: 4, + icon: 'news', render: () => } ]; @@ -243,6 +276,7 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] { export default function DashboardWidgetLibrary(): DashboardWidgetProps[] { return [ ...BuiltinQueryCountWidgets(), + ...BuiltinHistoryWidgets(), ...BuiltinGettingStartedWidgets(), ...BuiltinSettingsWidgets(), ...BuiltinActionWidgets() diff --git a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx new file mode 100644 index 0000000000..0823a720c6 --- /dev/null +++ b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx @@ -0,0 +1,131 @@ +import { ModelInformationDict } from '@lib/enums/ModelInformation'; +import type { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import { StylishText } from '@lib/index'; +import { t } from '@lingui/core/macro'; +import { BarChart } from '@mantine/charts'; +import { Stack } from '@mantine/core'; +import { useDocumentVisibility } from '@mantine/hooks'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useMemo } from 'react'; +import { useApi } from '../../../contexts/ApiContext'; +import { useUserState } from '../../../states/UserState'; +import type { DashboardWidgetProps } from '../DashboardWidget'; + +function OrderHistoryComponent({ + modelType, + title +}: { + modelType: ModelType; + title: string; +}) { + const modelInfo = useMemo(() => { + return ModelInformationDict[modelType]; + }, [modelType]); + + const url = useMemo(() => { + return apiUrl(modelInfo.api_endpoint); + }, [modelInfo]); + + const api = useApi(); + const visibility = useDocumentVisibility(); + + const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD'); + const startDate = dayjs() + .subtract(12, 'month') + .subtract(1, 'day') + .format('YYYY-MM-DD'); + + const params = { + completed_after: startDate, + completed_before: endDate + }; + + const query = useQuery({ + queryKey: ['dashboard-order-summary', modelType, params, visibility], + enabled: visibility === 'visible', + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval + staleTime: 5 * 60 * 1000, // 5 minute stale time + queryFn: () => { + if (visibility !== 'visible') { + return []; + } + + return api.get(url, { params }).then((res) => { + return res.data ?? []; + }); + } + }); + + const chartData = useMemo(() => { + const months = Array.from({ length: 13 }, (_, i) => ({ + month: dayjs() + .subtract(12 - i, 'month') + .format('MMM YY'), + count: 0 + })); + + for (const order of query.data || []) { + // Build orders use `completion_date`; all other order types use `complete_date` + const dateStr = + order.complete_date ?? order.completion_date ?? order.shipment_date; + if (!dateStr) continue; + const label = dayjs(dateStr).format('MMM YY'); + const entry = months.find((m) => m.month === label); + if (entry) entry.count++; + } + + return months; + }, [query.data]); + + return ( + + {title} + + + ); +} + +/** + * Display a simple chart of the number of completed orders per month, for the last 12 months. + */ +export default function OrderHistoryWidget({ + modelType +}: { + modelType: ModelType; +}): DashboardWidgetProps { + const user = useUserState(); + + const modelInfo = useMemo(() => { + return ModelInformationDict[modelType]; + }, [modelType]); + + // Extract translated model labels + const models = modelInfo.label_multiple(); + + return { + label: `${modelType}-history`, + title: t`Completed ${models}`, + description: t`Display number of completed ${models} per month`, + minHeight: 2, + minWidth: 3, + modelType: modelType, + icon: 'chart_bar', + visible: () => user.hasViewPermission(modelType), + render: () => ( + + ) + }; +} diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 5e6b0afc0b..bee2c7b431 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -129,6 +129,7 @@ export default function QueryCountDashboardWidget({ title, description, modelType, + icon, enabled = true, params }: { @@ -136,6 +137,7 @@ export default function QueryCountDashboardWidget({ title: string; description: string; modelType: ModelType; + icon?: keyof InvenTreeIconType; enabled?: boolean; params: any; }): DashboardWidgetProps { @@ -147,6 +149,7 @@ export default function QueryCountDashboardWidget({ modelType: modelType, minWidth: 2, minHeight: 1, + icon: icon, render: () => ( ) diff --git a/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx index 03e94af071..b4e3ae9c60 100644 --- a/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/StocktakeDashboardWidget.tsx @@ -66,6 +66,7 @@ export default function StocktakeDashboardWidget(): DashboardWidgetProps { minHeight: 1, minWidth: 2, render: () => , + icon: 'stocktake', enabled: user.hasAddRole(UserRoles.part) }; } diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index e169f89e79..bdc1826e89 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -15,10 +15,13 @@ import { IconCalendar, IconCalendarCheck, IconCalendarDot, + IconCalendarExclamation, IconCalendarStats, IconCalendarTime, IconCalendarX, IconCancel, + IconChartBar, + IconChartLine, IconCheck, IconCircleCheck, IconCircleDashedCheck, @@ -63,6 +66,7 @@ import { IconMapPin, IconMapPinHeart, IconMinusVertical, + IconNews, IconNotes, IconNumber123, IconNumbers, @@ -158,6 +162,7 @@ const icons: InvenTreeIconType = { transfer_orders: IconTransfer, sales_orders: IconTruckDelivery, scheduling: IconCalendarStats, + overdue: IconCalendarExclamation, scrap: IconCircleX, shipment: IconCubeSend, test_templates: IconTestPipe, @@ -267,7 +272,11 @@ const icons: InvenTreeIconType = { plugin: IconPlug, history: IconHistory, dashboard: IconLayoutDashboard, - search: IconSearch + search: IconSearch, + + chart_bar: IconChartBar, + chart_line: IconChartLine, + news: IconNews }; /**