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
};
/**