diff --git a/src/frontend/src/components/details/DetailsBadge.tsx b/src/frontend/src/components/details/DetailsBadge.tsx new file mode 100644 index 0000000000..d5228e9fd4 --- /dev/null +++ b/src/frontend/src/components/details/DetailsBadge.tsx @@ -0,0 +1,26 @@ +import { Badge } from '@mantine/core'; + +export type DetailsBadgeProps = { + color: string; + label: string; + size?: string; + visible?: boolean; + key?: any; +}; + +export default function DetailsBadge(props: DetailsBadgeProps) { + if (props.visible == false) { + return null; + } + + return ( + + {props.label} + + ); +} diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index efaf1f3b33..6b640253f5 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -1,6 +1,7 @@ import { Group, Paper, Space, Stack, Text } from '@mantine/core'; import { Fragment, ReactNode } from 'react'; +import DetailsBadge, { DetailsBadgeProps } from '../details/DetailsBadge'; import { ApiImage } from '../images/ApiImage'; import { StylishText } from '../items/StylishText'; import { Breadcrumb, BreadcrumbList } from './BreadcrumbList'; @@ -15,6 +16,7 @@ export function PageDetail({ title, subtitle, detail, + badges, imageUrl, breadcrumbs, breadcrumbAction, @@ -24,6 +26,7 @@ export function PageDetail({ subtitle?: string; imageUrl?: string; detail?: ReactNode; + badges?: ReactNode[]; breadcrumbs?: Breadcrumb[]; breadcrumbAction?: () => void; actions?: ReactNode[]; @@ -56,6 +59,9 @@ export function PageDetail({ {detail} + + {badges} + {actions && ( diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 58e36b61a1..52695ff463 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -334,16 +334,17 @@ export default function BuildDetail() { ]; }, [id, build, user]); - const buildDetail = useMemo(() => { - return build?.status ? ( - StatusRenderer({ - status: build.status, - type: ModelType.build - }) - ) : ( - - ); - }, [build, id]); + const buildBadges = useMemo(() => { + return instanceQuery.isFetching + ? [] + : [ + + ]; + }, [build, instanceQuery]); return ( <> @@ -353,7 +354,7 @@ export default function BuildDetail() { { - return ( - - - Stock: {part.in_stock} - - - ); - }, [part, id]); + const badges: ReactNode[] = useMemo(() => { + if (instanceQuery.isLoading || instanceQuery.isFetching) { + return []; + } + + return [ + = part.minimum_stock ? 'green' : 'orange'} + visible={part.in_stock > 0} + />, + , + 0} + />, + 0} + /> + ]; + }, [part, instanceQuery]); const partFields = usePartFields({ create: false }); @@ -740,7 +763,7 @@ export default function PartDetail() { title={t`Part` + ': ' + part.full_name} subtitle={part.description} imageUrl={part.image} - detail={partDetail} + badges={badges} breadcrumbs={breadcrumbs} breadcrumbAction={() => { setTreeOpen(true); diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 74a4fb40aa..ecf43cd7bb 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Group, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { IconDots, IconInfoCircle, @@ -8,7 +8,7 @@ import { IconPackages, IconPaperclip } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { DetailsField, DetailsTable } from '../../components/details/Details'; @@ -25,6 +25,7 @@ import { } from '../../components/items/ActionDropdown'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; @@ -297,6 +298,18 @@ export default function PurchaseOrderDetail() { ]; }, [id, order, user]); + const orderBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + + ]; + }, [order, instanceQuery]); + return ( <> {editPurchaseOrder.modal} @@ -308,6 +321,7 @@ export default function PurchaseOrderDetail() { imageUrl={order.supplier_detail?.image} breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]} actions={poActions} + badges={orderBadges} /> diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 4d951172bc..2455488673 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -6,7 +6,7 @@ import { IconNotes, IconPaperclip } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { DetailsField, DetailsTable } from '../../components/details/Details'; @@ -14,6 +14,7 @@ import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; @@ -220,6 +221,18 @@ export default function ReturnOrderDetail() { ]; }, [order, id]); + const orderBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + + ]; + }, [order, instanceQuery]); + return ( <> @@ -228,6 +241,7 @@ export default function ReturnOrderDetail() { title={t`Return Order` + `: ${order.reference}`} subtitle={order.description} imageUrl={order.customer_detail?.image} + badges={orderBadges} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} /> diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index aecb19c42d..182c0fbfde 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -9,7 +9,7 @@ import { IconTruckDelivery, IconTruckLoading } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { DetailsField, DetailsTable } from '../../components/details/Details'; @@ -17,6 +17,7 @@ import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; @@ -244,6 +245,18 @@ export default function SalesOrderDetail() { ]; }, [order, id]); + const orderBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + + ]; + }, [order, instanceQuery]); + return ( <> @@ -252,6 +265,7 @@ export default function SalesOrderDetail() { title={t`Sales Order` + `: ${order.reference}`} subtitle={order.description} imageUrl={order.customer_detail?.image} + badges={orderBadges} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} /> diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 3806d5c17b..072b881823 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -1,7 +1,9 @@ import { t } from '@lingui/macro'; import { Alert, + Badge, Grid, + Group, LoadingOverlay, Skeleton, Stack, @@ -20,10 +22,11 @@ import { IconPaperclip, IconSitemap } from '@tabler/icons-react'; -import { useMemo, useState } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { DetailsField, DetailsTable } from '../../components/details/Details'; +import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { @@ -38,6 +41,7 @@ import { import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StockLocationTree } from '../../components/nav/StockLocationTree'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; @@ -437,6 +441,33 @@ export default function StockDetail() { [id, stockitem, user] ); + const stockBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + , + , + , + + ]; + }, [stockitem, instanceQuery]); + return ( @@ -449,11 +480,7 @@ export default function StockDetail() { title={t`Stock Item`} subtitle={stockitem.part_detail?.full_name} imageUrl={stockitem.part_detail?.thumbnail} - detail={ - - Quantity: {stockitem.quantity ?? 'idk'} - - } + badges={stockBadges} breadcrumbs={breadcrumbs} breadcrumbAction={() => { setTreeOpen(true); diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index f27299bc82..6de23f03bc 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -91,6 +91,7 @@ export type InvenTreeTableProps = { onRowClick?: (record: T, index: number, event: any) => void; onCellClick?: DataTableCellClickHandler; modelType?: ModelType; + modelField?: string; }; /** @@ -515,18 +516,22 @@ export function InvenTreeTable({ if (props.onRowClick) { // If a custom row click handler is provided, use that props.onRowClick(record, index, event); - } else if (tableProps.modelType && record?.pk) { - // If a model type is provided, navigate to the detail view for that model - let url = getDetailUrl(tableProps.modelType, record.pk); + } else if (tableProps.modelType) { + const pk = record?.[tableProps.modelField ?? 'pk']; - // Should it be opened in a new tab? - if (event?.ctrlKey || event?.shiftKey) { - // Open in a new tab - url = `/${base_url}${url}`; - window.open(url, '_blank'); - } else { - // Navigate internally - navigate(url); + if (pk) { + // If a model type is provided, navigate to the detail view for that model + let url = getDetailUrl(tableProps.modelType, pk); + + // Should it be opened in a new tab? + if (event?.ctrlKey || event?.shiftKey) { + // Open in a new tab + url = `/${base_url}${url}`; + window.open(url, '_blank'); + } else { + // Navigate internally + navigate(url); + } } } }, diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx index 1033e0effb..6f62a7ef31 100644 --- a/src/frontend/src/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/tables/bom/UsedInTable.tsx @@ -83,7 +83,8 @@ export function UsedInTable({ sub_part_detail: true }, tableFilters: tableFilters, - modelType: ModelType.part + modelType: ModelType.part, + modelField: 'part' }} /> ); diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index ee2baa620f..80a41bdcfe 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -234,6 +234,10 @@ export default function ParametricPartTable({ { accessor: 'IPN', sortable: true + }, + { + accessor: 'total_in_stock', + sortable: true } ];