2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-06 08:54:24 +00:00

[UI] Order history dashboard widgets (#12061)

* Add order history dashboard widgets

* Support icons for dashboard widgets

* Change StylishText size from 'lg' to 'md'
This commit is contained in:
Oliver
2026-06-01 21:44:25 +10:00
committed by GitHub
parent 7f86384a03
commit d38af61ba2
7 changed files with 193 additions and 12 deletions
@@ -5,6 +5,7 @@ import { IconX } from '@tabler/icons-react';
import { Boundary } from '@lib/components/Boundary'; import { Boundary } from '@lib/components/Boundary';
import type { ModelType } from '@lib/index'; import type { ModelType } from '@lib/index';
import type { InvenTreeIconType } from '@lib/types/Icons';
import type { JSX } from 'react'; import type { JSX } from 'react';
/** /**
@@ -22,6 +23,7 @@ export interface DashboardWidgetProps {
minWidth?: number; minWidth?: number;
minHeight?: number; minHeight?: number;
modelType?: ModelType; modelType?: ModelType;
icon?: keyof InvenTreeIconType;
render: () => JSX.Element; render: () => JSX.Element;
visible?: () => boolean; visible?: () => boolean;
} }
@@ -12,10 +12,11 @@ import {
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { IconBackspace, IconLayoutGridAdd } from '@tabler/icons-react'; import { IconBackspace } from '@tabler/icons-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { StylishText } from '@lib/components/StylishText'; import { StylishText } from '@lib/components/StylishText';
import { InvenTreeIcon } from '../../functions/icons';
import { useDashboardItems } from '../../hooks/UseDashboardItems'; import { useDashboardItems } from '../../hooks/UseDashboardItems';
/** /**
@@ -105,7 +106,7 @@ export default function DashboardWidgetDrawer({
onAddWidget(widget.label); onAddWidget(widget.label);
}} }}
> >
<IconLayoutGridAdd /> <InvenTreeIcon icon={widget.icon ?? 'dashboard'} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
@@ -8,6 +8,7 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget';
import GetStartedWidget from './widgets/GetStartedWidget'; import GetStartedWidget from './widgets/GetStartedWidget';
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';
import NewsWidget from './widgets/NewsWidget'; import NewsWidget from './widgets/NewsWidget';
import OrderHistoryWidget from './widgets/OrderHistoryWidget';
import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget';
import QueryDashboardWidget from './widgets/QueryDashboardWidget'; import QueryDashboardWidget from './widgets/QueryDashboardWidget';
import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget'; import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget';
@@ -43,6 +44,7 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
title: t`Invalid BOMs`, title: t`Invalid BOMs`,
description: t`Assemblies requiring bill of materials validation`, description: t`Assemblies requiring bill of materials validation`,
modelType: ModelType.part, modelType: ModelType.part,
icon: 'exclamation',
params: { params: {
active: true, // Only show active parts active: true, // Only show active parts
assembly: true, // Only show parts which are assemblies 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`, description: t`Show the number of stock items which have expired`,
modelType: ModelType.stockitem, modelType: ModelType.stockitem,
params: { expired: true }, params: { expired: true },
enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY') enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
icon: 'overdue'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Stale Stock Items`, title: t`Stale Stock Items`,
@@ -116,14 +119,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
label: 'ovr-bo', label: 'ovr-bo',
description: t`Show the number of build orders which are overdue`, description: t`Show the number of build orders which are overdue`,
modelType: ModelType.build, modelType: ModelType.build,
params: { overdue: true } params: { overdue: true },
icon: 'overdue'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Assigned Build Orders`, title: t`Assigned Build Orders`,
label: 'asn-bo', label: 'asn-bo',
description: t`Show the number of build orders which are assigned to you`, description: t`Show the number of build orders which are assigned to you`,
modelType: ModelType.build, modelType: ModelType.build,
params: { assigned_to_me: true, outstanding: true } params: { assigned_to_me: true, outstanding: true },
icon: 'responsible'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Active Sales Orders`, title: t`Active Sales Orders`,
@@ -137,14 +142,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
label: 'ovr-so', label: 'ovr-so',
description: t`Show the number of sales orders which are overdue`, description: t`Show the number of sales orders which are overdue`,
modelType: ModelType.salesorder, modelType: ModelType.salesorder,
params: { overdue: true } params: { overdue: true },
icon: 'overdue'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Assigned Sales Orders`, title: t`Assigned Sales Orders`,
label: 'asn-so', label: 'asn-so',
description: t`Show the number of sales orders which are assigned to you`, description: t`Show the number of sales orders which are assigned to you`,
modelType: ModelType.salesorder, modelType: ModelType.salesorder,
params: { assigned_to_me: true, outstanding: true } params: { assigned_to_me: true, outstanding: true },
icon: 'responsible'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Pending Shipments`, title: t`Pending Shipments`,
@@ -165,14 +172,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
label: 'ovr-po', label: 'ovr-po',
description: t`Show the number of purchase orders which are overdue`, description: t`Show the number of purchase orders which are overdue`,
modelType: ModelType.purchaseorder, modelType: ModelType.purchaseorder,
params: { overdue: true } params: { overdue: true },
icon: 'overdue'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Assigned Purchase Orders`, title: t`Assigned Purchase Orders`,
label: 'asn-po', label: 'asn-po',
description: t`Show the number of purchase orders which are assigned to you`, description: t`Show the number of purchase orders which are assigned to you`,
modelType: ModelType.purchaseorder, modelType: ModelType.purchaseorder,
params: { assigned_to_me: true, outstanding: true } params: { assigned_to_me: true, outstanding: true },
icon: 'responsible'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Active Return Orders`, title: t`Active Return Orders`,
@@ -186,14 +195,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
label: 'ovr-ro', label: 'ovr-ro',
description: t`Show the number of return orders which are overdue`, description: t`Show the number of return orders which are overdue`,
modelType: ModelType.returnorder, modelType: ModelType.returnorder,
params: { overdue: true } params: { overdue: true },
icon: 'overdue'
}), }),
QueryCountDashboardWidget({ QueryCountDashboardWidget({
title: t`Assigned Return Orders`, title: t`Assigned Return Orders`,
label: 'asn-ro', label: 'asn-ro',
description: t`Show the number of return orders which are assigned to you`, description: t`Show the number of return orders which are assigned to you`,
modelType: ModelType.returnorder, 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[] { function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
return [ return [
{ {
@@ -215,6 +246,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
description: t`Getting started with InvenTree`, description: t`Getting started with InvenTree`,
minWidth: 5, minWidth: 5,
minHeight: 4, minHeight: 4,
icon: 'info',
render: () => <GetStartedWidget /> render: () => <GetStartedWidget />
}, },
{ {
@@ -223,6 +255,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
description: t`The latest news from InvenTree`, description: t`The latest news from InvenTree`,
minWidth: 5, minWidth: 5,
minHeight: 4, minHeight: 4,
icon: 'news',
render: () => <NewsWidget /> render: () => <NewsWidget />
} }
]; ];
@@ -243,6 +276,7 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] {
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] { export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
return [ return [
...BuiltinQueryCountWidgets(), ...BuiltinQueryCountWidgets(),
...BuiltinHistoryWidgets(),
...BuiltinGettingStartedWidgets(), ...BuiltinGettingStartedWidgets(),
...BuiltinSettingsWidgets(), ...BuiltinSettingsWidgets(),
...BuiltinActionWidgets() ...BuiltinActionWidgets()
@@ -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 (
<Stack gap='xs'>
<StylishText size='md'>{title}</StylishText>
<BarChart
h={200}
data={chartData}
dataKey='month'
series={[{ name: 'count', label: t`Completed`, color: 'blue.6' }]}
withYAxis={false}
/>
</Stack>
);
}
/**
* 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: () => (
<OrderHistoryComponent
modelType={modelType}
title={t`Completed ${models}`}
/>
)
};
}
@@ -129,6 +129,7 @@ export default function QueryCountDashboardWidget({
title, title,
description, description,
modelType, modelType,
icon,
enabled = true, enabled = true,
params params
}: { }: {
@@ -136,6 +137,7 @@ export default function QueryCountDashboardWidget({
title: string; title: string;
description: string; description: string;
modelType: ModelType; modelType: ModelType;
icon?: keyof InvenTreeIconType;
enabled?: boolean; enabled?: boolean;
params: any; params: any;
}): DashboardWidgetProps { }): DashboardWidgetProps {
@@ -147,6 +149,7 @@ export default function QueryCountDashboardWidget({
modelType: modelType, modelType: modelType,
minWidth: 2, minWidth: 2,
minHeight: 1, minHeight: 1,
icon: icon,
render: () => ( render: () => (
<QueryCountWidget modelType={modelType} title={title} params={params} /> <QueryCountWidget modelType={modelType} title={title} params={params} />
) )
@@ -66,6 +66,7 @@ export default function StocktakeDashboardWidget(): DashboardWidgetProps {
minHeight: 1, minHeight: 1,
minWidth: 2, minWidth: 2,
render: () => <StocktakeWidget />, render: () => <StocktakeWidget />,
icon: 'stocktake',
enabled: user.hasAddRole(UserRoles.part) enabled: user.hasAddRole(UserRoles.part)
}; };
} }
+10 -1
View File
@@ -15,10 +15,13 @@ import {
IconCalendar, IconCalendar,
IconCalendarCheck, IconCalendarCheck,
IconCalendarDot, IconCalendarDot,
IconCalendarExclamation,
IconCalendarStats, IconCalendarStats,
IconCalendarTime, IconCalendarTime,
IconCalendarX, IconCalendarX,
IconCancel, IconCancel,
IconChartBar,
IconChartLine,
IconCheck, IconCheck,
IconCircleCheck, IconCircleCheck,
IconCircleDashedCheck, IconCircleDashedCheck,
@@ -63,6 +66,7 @@ import {
IconMapPin, IconMapPin,
IconMapPinHeart, IconMapPinHeart,
IconMinusVertical, IconMinusVertical,
IconNews,
IconNotes, IconNotes,
IconNumber123, IconNumber123,
IconNumbers, IconNumbers,
@@ -158,6 +162,7 @@ const icons: InvenTreeIconType = {
transfer_orders: IconTransfer, transfer_orders: IconTransfer,
sales_orders: IconTruckDelivery, sales_orders: IconTruckDelivery,
scheduling: IconCalendarStats, scheduling: IconCalendarStats,
overdue: IconCalendarExclamation,
scrap: IconCircleX, scrap: IconCircleX,
shipment: IconCubeSend, shipment: IconCubeSend,
test_templates: IconTestPipe, test_templates: IconTestPipe,
@@ -267,7 +272,11 @@ const icons: InvenTreeIconType = {
plugin: IconPlug, plugin: IconPlug,
history: IconHistory, history: IconHistory,
dashboard: IconLayoutDashboard, dashboard: IconLayoutDashboard,
search: IconSearch search: IconSearch,
chart_bar: IconChartBar,
chart_line: IconChartLine,
news: IconNews
}; };
/** /**