From 76b298c43e7def9cf01c2226cdbbf1bab0d3a5bc Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 21 May 2024 21:06:02 +1000 Subject: [PATCH] [PUI] StockTrackingTable (#7273) * Bare bones component * Implement details panel for StockTrackingTable * Remove unused userState hook * Expand RenderInstance to include link * Allow inline renderers to display links --- src/frontend/src/components/render/Build.tsx | 11 +- .../src/components/render/Company.tsx | 50 ++-- .../src/components/render/Instance.tsx | 61 +++-- src/frontend/src/components/render/Order.tsx | 47 ++-- src/frontend/src/components/render/Part.tsx | 28 ++- src/frontend/src/components/render/Stock.tsx | 27 ++- src/frontend/src/pages/stock/StockDetail.tsx | 8 +- .../src/tables/stock/StockTrackingTable.tsx | 220 ++++++++++++++++++ 8 files changed, 379 insertions(+), 73 deletions(-) create mode 100644 src/frontend/src/tables/stock/StockTrackingTable.tsx diff --git a/src/frontend/src/components/render/Build.tsx b/src/frontend/src/components/render/Build.tsx index 2f40f912e4..37f8eacde0 100644 --- a/src/frontend/src/components/render/Build.tsx +++ b/src/frontend/src/components/render/Build.tsx @@ -1,17 +1,21 @@ import { ReactNode } from 'react'; import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; import { InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; /** * Inline rendering of a single BuildOrder instance */ -export function RenderBuildOrder({ - instance -}: Readonly): ReactNode { +export function RenderBuildOrder( + props: Readonly +): ReactNode { + const { instance } = props; + return ( ); } diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index 9dce6888d2..524d140851 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -1,5 +1,7 @@ import { ReactNode } from 'react'; +import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** @@ -25,16 +27,20 @@ export function RenderAddress({ /** * Inline rendering of a single Company instance */ -export function RenderCompany({ - instance -}: Readonly): ReactNode { - // TODO: Handle URL +export function RenderCompany( + props: Readonly +): ReactNode { + const { instance } = props; return ( ); } @@ -51,20 +57,25 @@ export function RenderContact({ /** * Inline rendering of a single SupplierPart instance */ -export function RenderSupplierPart({ - instance -}: Readonly): ReactNode { - // TODO: handle URL - - let supplier = instance.supplier_detail ?? {}; - let part = instance.part_detail ?? {}; +export function RenderSupplierPart( + props: Readonly +): ReactNode { + const { instance } = props; + const supplier = instance.supplier_detail ?? {}; + const part = instance.part_detail ?? {}; return ( ); } @@ -72,18 +83,25 @@ export function RenderSupplierPart({ /** * Inline rendering of a single ManufacturerPart instance */ -export function RenderManufacturerPart({ - instance -}: Readonly): ReactNode { - let part = instance.part_detail ?? {}; - let manufacturer = instance.manufacturer_detail ?? {}; +export function RenderManufacturerPart( + props: Readonly +): ReactNode { + const { instance } = props; + const part = instance.part_detail ?? {}; + const manufacturer = instance.manufacturer_detail ?? {}; return ( ); } diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index fefb2ddb91..905882ccfe 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -1,8 +1,9 @@ import { t } from '@lingui/macro'; -import { Alert, Group, Space, Text } from '@mantine/core'; -import { ReactNode } from 'react'; +import { Alert, Anchor, Group, Space, Text } from '@mantine/core'; +import { ReactNode, useCallback } from 'react'; import { ModelType } from '../../enums/ModelType'; +import { navigateToLink } from '../../functions/navigation'; import { Thumbnail } from '../images/Thumbnail'; import { RenderBuildLine, RenderBuildOrder } from './Build'; import { @@ -36,6 +37,12 @@ type EnumDictionary = { [K in T]: U; }; +export interface InstanceRenderInterface { + instance: any; + link?: boolean; + navigate?: any; +} + /** * Lookup table for rendering a model instance */ @@ -68,31 +75,27 @@ const RendererLookup: EnumDictionary< [ModelType.user]: RenderUser }; -// import { ApiFormFieldType } from "../forms/fields/ApiFormField"; +export type RenderInstanceProps = { + model: ModelType | undefined; +} & InstanceRenderInterface; /** * Render an instance of a database model, depending on the provided data */ -export function RenderInstance({ - model, - instance -}: { - model: ModelType | undefined; - instance: any; -}): ReactNode { - if (model === undefined) { +export function RenderInstance(props: RenderInstanceProps): ReactNode { + if (props.model === undefined) { console.error('RenderInstance: No model provided'); - return ; + return ; } - const RenderComponent = RendererLookup[model]; + const RenderComponent = RendererLookup[props.model]; if (!RenderComponent) { - console.error(`RenderInstance: No renderer for model ${model}`); - return ; + console.error(`RenderInstance: No renderer for model ${props.model}`); + return ; } - return ; + return ; } /** @@ -104,7 +107,8 @@ export function RenderInlineModel({ suffix, image, labels, - url + url, + navigate }: { primary: string; secondary?: string; @@ -112,15 +116,30 @@ export function RenderInlineModel({ image?: string; labels?: string[]; url?: string; + navigate?: any; }): ReactNode { // TODO: Handle labels - // TODO: Handle URL + + const onClick = useCallback( + (event: any) => { + if (url && navigate) { + navigateToLink(url, navigate, event); + } + }, + [url, navigate] + ); return ( {image && Thumbnail({ src: image, size: 18 })} - {primary} + {url ? ( + onClick(event)}> + {primary} + + ) : ( + {primary} + )} {secondary && {secondary}} {suffix && ( @@ -144,7 +163,3 @@ export function UnknownRenderer({ ); } - -export interface InstanceRenderInterface { - instance: any; -} diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index 63cdd581e5..d36277dfe4 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -2,20 +2,22 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; import { InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; /** * Inline rendering of a single PurchaseOrder instance */ -export function RenderPurchaseOrder({ - instance -}: Readonly): ReactNode { - let supplier = instance.supplier_detail || {}; +export function RenderPurchaseOrder( + props: Readonly +): ReactNode { + const { instance } = props; + const supplier = instance?.supplier_detail || {}; - // TODO: Handle URL return ( ); } @@ -30,13 +37,15 @@ export function RenderPurchaseOrder({ /** * Inline rendering of a single ReturnOrder instance */ -export function RenderReturnOrder({ - instance -}: Readonly): ReactNode { - let customer = instance.customer_detail || {}; +export function RenderReturnOrder( + props: Readonly +): ReactNode { + const { instance } = props; + const customer = instance?.customer_detail || {}; return ( ); } @@ -51,15 +65,15 @@ export function RenderReturnOrder({ /** * Inline rendering of a single SalesOrder instance */ -export function RenderSalesOrder({ - instance -}: Readonly): ReactNode { - let customer = instance.customer_detail || {}; - - // TODO: Handle URL +export function RenderSalesOrder( + props: Readonly +): ReactNode { + const { instance } = props; + const customer = instance?.customer_detail || {}; return ( ); } diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index c2e0ed382b..fa35f697dd 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -1,22 +1,27 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; +import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** * Inline rendering of a single Part instance */ -export function RenderPart({ - instance -}: Readonly): ReactNode { +export function RenderPart( + props: Readonly +): ReactNode { + const { instance } = props; const stock = t`Stock` + `: ${instance.in_stock}`; return ( ); } @@ -24,17 +29,22 @@ export function RenderPart({ /** * Inline rendering of a PartCategory instance */ -export function RenderPartCategory({ - instance -}: Readonly): ReactNode { - // TODO: Handle URL - - let lvl = '-'.repeat(instance.level || 0); +export function RenderPartCategory( + props: Readonly +): ReactNode { + const { instance } = props; + const lvl = '-'.repeat(instance.level || 0); return ( ); } diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index 4555cee4c6..1af29e0f23 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -1,18 +1,28 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; +import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** * Inline rendering of a single StockLocation instance */ -export function RenderStockLocation({ - instance -}: Readonly): ReactNode { +export function RenderStockLocation( + props: Readonly +): ReactNode { + const { instance } = props; + return ( ); } @@ -32,9 +42,10 @@ export function RenderStockLocationType({ ); } -export function RenderStockItem({ - instance -}: Readonly): ReactNode { +export function RenderStockItem( + props: Readonly +): ReactNode { + const { instance } = props; let quantity_string = ''; if (instance?.serial !== null && instance?.serial !== undefined) { @@ -45,9 +56,13 @@ export function RenderStockItem({ return ( ); } diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 6cc5abbf38..205f419aba 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -60,6 +60,7 @@ import { AttachmentTable } from '../../tables/general/AttachmentTable'; import InstalledItemsTable from '../../tables/stock/InstalledItemsTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable'; +import { StockTrackingTable } from '../../tables/stock/StockTrackingTable'; export default function StockDetail() { const { id } = useParams(); @@ -269,7 +270,12 @@ export default function StockDetail() { { name: 'tracking', label: t`Stock Tracking`, - icon: + icon: , + content: stockitem.pk ? ( + + ) : ( + + ) }, { name: 'allocations', diff --git a/src/frontend/src/tables/stock/StockTrackingTable.tsx b/src/frontend/src/tables/stock/StockTrackingTable.tsx new file mode 100644 index 0000000000..208c6f2b98 --- /dev/null +++ b/src/frontend/src/tables/stock/StockTrackingTable.tsx @@ -0,0 +1,220 @@ +import { t } from '@lingui/macro'; +import { Table, Text } from '@mantine/core'; +import { ReactNode, useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { RenderBuildOrder } from '../../components/render/Build'; +import { RenderCompany } from '../../components/render/Company'; +import { + RenderPurchaseOrder, + RenderReturnOrder, + RenderSalesOrder +} from '../../components/render/Order'; +import { RenderPart } from '../../components/render/Part'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; +import { + RenderStockItem, + RenderStockLocation +} from '../../components/render/Stock'; +import { RenderUser } from '../../components/render/User'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { TableColumn } from '../Column'; +import { DateColumn, DescriptionColumn } from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; + +type StockTrackingEntry = { + label: string; + key: string; + details: ReactNode; +}; + +export function StockTrackingTable({ itemId }: { itemId: number }) { + const navigate = useNavigate(); + const table = useTable('stock_tracking'); + + // Render "details" for a stock tracking record + const renderDetails = useCallback( + (record: any) => { + const deltas: any = record?.deltas ?? {}; + + let entries: StockTrackingEntry[] = [ + { + label: t`Stock Item`, + key: 'stockitem', + details: + deltas.stockitem_detail && + RenderStockItem({ instance: deltas.stockitem_detail }) + }, + { + label: t`Status`, + key: 'status', + details: + deltas.status && + StatusRenderer({ status: deltas.status, type: ModelType.stockitem }) + }, + { + label: t`Quantity`, + key: 'quantity', + details: deltas.quantity + }, + { + label: t`Added`, + key: 'added', + details: deltas.added + }, + { + label: t`Removed`, + key: 'removed', + details: deltas.removed + }, + { + label: t`Part`, + key: 'part', + details: + deltas.part_detail && + RenderPart({ + instance: deltas.part_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Location`, + key: 'location', + details: + deltas.location_detail && + RenderStockLocation({ + instance: deltas.location_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Build Order`, + key: 'buildorder', + details: + deltas.buildorder_detail && + RenderBuildOrder({ + instance: deltas.buildorder_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Purchase Order`, + key: 'purchaseorder', + details: + deltas.purchaseorder_detail && + RenderPurchaseOrder({ + instance: deltas.purchaseorder_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Sales Order`, + key: 'salesorder', + details: + deltas.salesorder_detail && + RenderSalesOrder({ + instance: deltas.salesorder_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Return Order`, + key: 'returnorder', + details: + deltas.returnorder_detail && + RenderReturnOrder({ + instance: deltas.returnorder_detail, + link: true, + navigate: navigate + }) + }, + { + label: t`Customer`, + key: 'customer', + details: + deltas.customer_detail && + RenderCompany({ + instance: deltas.customer_detail, + link: true, + navigate: navigate + }) + } + ]; + + return ( + + + {entries.map( + (entry) => + entry.details && ( + + + {entry.label} + + {entry.details} + + ) + )} + +
+ ); + }, + [navigate] + ); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + DateColumn({ + switchable: false + }), + DescriptionColumn({ + accessor: 'label' + }), + { + accessor: 'details', + title: t`Details`, + switchable: false, + render: renderDetails + }, + { + accessor: 'notes', + title: t`Notes`, + sortable: false, + switchable: true + }, + { + accessor: 'user', + title: t`User`, + render: (record: any) => { + if (!record.user_detail) { + return {t`No user information`}; + } + + return RenderUser({ instance: record.user_detail }); + } + } + ]; + }, []); + + return ( + + ); +}