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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
<IconLayoutGridAdd />
|
||||
<InvenTreeIcon icon={widget.icon ?? 'dashboard'} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
|
||||
@@ -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: () => <GetStartedWidget />
|
||||
},
|
||||
{
|
||||
@@ -223,6 +255,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
||||
description: t`The latest news from InvenTree`,
|
||||
minWidth: 5,
|
||||
minHeight: 4,
|
||||
icon: 'news',
|
||||
render: () => <NewsWidget />
|
||||
}
|
||||
];
|
||||
@@ -243,6 +276,7 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] {
|
||||
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
|
||||
return [
|
||||
...BuiltinQueryCountWidgets(),
|
||||
...BuiltinHistoryWidgets(),
|
||||
...BuiltinGettingStartedWidgets(),
|
||||
...BuiltinSettingsWidgets(),
|
||||
...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,
|
||||
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: () => (
|
||||
<QueryCountWidget modelType={modelType} title={title} params={params} />
|
||||
)
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function StocktakeDashboardWidget(): DashboardWidgetProps {
|
||||
minHeight: 1,
|
||||
minWidth: 2,
|
||||
render: () => <StocktakeWidget />,
|
||||
icon: 'stocktake',
|
||||
enabled: user.hasAddRole(UserRoles.part)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user