mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	React updates (#5826)
* Add more panels to StockItem page * Add some placeholder actions for StockItem page * edit stock item * Add info hover card to stocktable * update extra info for part table * Add extra columns to PurchaseOrder table * Fix unused import
This commit is contained in:
		| @@ -1,4 +1,6 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { ActionIcon, Menu, Tooltip } from '@mantine/core'; | ||||
| import { IconQrcode } from '@tabler/icons-react'; | ||||
| import { ReactNode, useMemo } from 'react'; | ||||
|  | ||||
| import { notYetImplemented } from '../../functions/notifications'; | ||||
| @@ -32,7 +34,7 @@ export function ActionDropdown({ | ||||
|   return hasActions ? ( | ||||
|     <Menu position="bottom-end"> | ||||
|       <Menu.Target> | ||||
|         <Tooltip label={tooltip}> | ||||
|         <Tooltip label={tooltip} hidden={!tooltip}> | ||||
|           <ActionIcon size="lg" radius="sm" variant="outline"> | ||||
|             {icon} | ||||
|           </ActionIcon> | ||||
| @@ -63,3 +65,19 @@ export function ActionDropdown({ | ||||
|     </Menu> | ||||
|   ) : null; | ||||
| } | ||||
|  | ||||
| // Dropdown menu for barcode actions | ||||
| export function BarcodeActionDropdown({ | ||||
|   actions | ||||
| }: { | ||||
|   actions: ActionDropdownItem[]; | ||||
| }) { | ||||
|   return ( | ||||
|     <ActionDropdown | ||||
|       key="barcode" | ||||
|       tooltip={t`Barcode Actions`} | ||||
|       icon={<IconQrcode />} | ||||
|       actions={actions} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -75,16 +75,19 @@ function partTableColumns(): TableColumn[] { | ||||
|         let extra: ReactNode[] = []; | ||||
|  | ||||
|         let stock = record?.total_in_stock ?? 0; | ||||
|         let allocated = | ||||
|           (record?.allocated_to_build_orders ?? 0) + | ||||
|           (record?.allocated_to_sales_orders ?? 0); | ||||
|         let available = Math.max(0, stock - allocated); | ||||
|         let min_stock = record?.minimum_stock ?? 0; | ||||
|  | ||||
|         let text = String(stock); | ||||
|  | ||||
|         let color: string | undefined = undefined; | ||||
|  | ||||
|         if (record.minimum_stock > stock) { | ||||
|         if (min_stock > stock) { | ||||
|           extra.push( | ||||
|             <Text color="orange"> | ||||
|               {t`Minimum stock` + `: ${record.minimum_stock}`} | ||||
|             </Text> | ||||
|             <Text color="orange">{t`Minimum stock` + `: ${min_stock}`}</Text> | ||||
|           ); | ||||
|  | ||||
|           color = 'orange'; | ||||
| @@ -116,11 +119,19 @@ function partTableColumns(): TableColumn[] { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         // TODO: Add extra information on stock "deman" | ||||
|         if (available != stock) { | ||||
|           extra.push(<Text>{t`Available` + `: ${available}`}</Text>); | ||||
|         } | ||||
|  | ||||
|         if (stock == 0) { | ||||
|         // TODO: Add extra information on stock "demand" | ||||
|  | ||||
|         if (stock <= 0) { | ||||
|           color = 'red'; | ||||
|           text = t`No stock`; | ||||
|         } else if (available <= 0) { | ||||
|           color = 'orange'; | ||||
|         } else if (available < min_stock) { | ||||
|           color = 'yellow'; | ||||
|         } | ||||
|  | ||||
|         return ( | ||||
| @@ -129,7 +140,7 @@ function partTableColumns(): TableColumn[] { | ||||
|               <Group spacing="xs" position="left"> | ||||
|                 <Text color={color}>{text}</Text> | ||||
|                 {record.units && ( | ||||
|                   <Text size="xs" color="color"> | ||||
|                   <Text size="xs" color={color}> | ||||
|                     [{record.units}] | ||||
|                   </Text> | ||||
|                 )} | ||||
|   | ||||
| @@ -61,7 +61,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) { | ||||
|         accessor: 'project_code', | ||||
|         title: t`Project Code`, | ||||
|         switchable: true | ||||
|         // TODO: Custom formatter | ||||
|         // TODO: Custom project code formatter | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'status', | ||||
| @@ -78,22 +78,34 @@ export function PurchaseOrderTable({ params }: { params?: any }) { | ||||
|         accessor: 'creation_date', | ||||
|         title: t`Created`, | ||||
|         switchable: true | ||||
|         // TODO: Custom formatter | ||||
|         // TODO: Custom date formatter | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'target_date', | ||||
|         title: t`Target Date`, | ||||
|         switchable: true | ||||
|         // TODO: Custom formatter | ||||
|         // TODO: Custom date formatter | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'line_items', | ||||
|         title: t`Line Items`, | ||||
|         sortable: true, | ||||
|         switchable: true | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'total_price', | ||||
|         title: t`Total Price`, | ||||
|         sortable: true, | ||||
|         switchable: true | ||||
|         // TODO: Custom money formatter | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'responsible', | ||||
|         title: t`Responsible`, | ||||
|         sortable: true, | ||||
|         switchable: true | ||||
|         // TODO: custom 'owner' formatter | ||||
|       } | ||||
|       // TODO: total_price | ||||
|       // TODO: responsible | ||||
|     ]; | ||||
|   }, []); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Group, Text } from '@mantine/core'; | ||||
| import { useMemo } from 'react'; | ||||
| import { Group, Stack, Text } from '@mantine/core'; | ||||
| import { ReactNode, useMemo } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| import { notYetImplemented } from '../../../functions/notifications'; | ||||
| @@ -12,6 +12,7 @@ import { TableStatusRenderer } from '../../renderers/StatusRenderer'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { TableFilter } from '../Filter'; | ||||
| import { RowAction } from '../RowActions'; | ||||
| import { TableHoverCard } from '../TableHoverCard'; | ||||
| import { InvenTreeTable } from './../InvenTreeTable'; | ||||
|  | ||||
| /** | ||||
| @@ -46,8 +47,109 @@ function stockItemTableColumns(): TableColumn[] { | ||||
|     { | ||||
|       accessor: 'quantity', | ||||
|       sortable: true, | ||||
|       title: t`Stock` | ||||
|       // TODO: Custom renderer for stock quantity | ||||
|       title: t`Stock`, | ||||
|       render: (record) => { | ||||
|         // TODO: Push this out into a custom renderer | ||||
|         let quantity = record?.quantity ?? 0; | ||||
|         let allocated = record?.allocated ?? 0; | ||||
|         let available = quantity - allocated; | ||||
|         let text = quantity; | ||||
|         let part = record?.part_detail ?? {}; | ||||
|         let extra: ReactNode[] = []; | ||||
|         let color = undefined; | ||||
|  | ||||
|         if (record.serial && quantity == 1) { | ||||
|           text = `# ${record.serial}`; | ||||
|         } | ||||
|  | ||||
|         if (record.is_building) { | ||||
|           color = 'blue'; | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item is in production`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (record.sales_order) { | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item has been assigned to a sales order`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (record.customer) { | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item has been assigned to a customer`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (record.belongs_to) { | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item is installed in another stock item`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (record.consumed_by) { | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item has been consumed by a build order`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (record.expired) { | ||||
|           extra.push(<Text size="sm">{t`This stock item has expired`}</Text>); | ||||
|         } else if (record.stale) { | ||||
|           extra.push(<Text size="sm">{t`This stock item is stale`}</Text>); | ||||
|         } | ||||
|  | ||||
|         if (allocated > 0) { | ||||
|           if (allocated >= quantity) { | ||||
|             color = 'orange'; | ||||
|             extra.push( | ||||
|               <Text size="sm">{t`This stock item is fully allocated`}</Text> | ||||
|             ); | ||||
|           } else { | ||||
|             extra.push( | ||||
|               <Text size="sm">{t`This stock item is partially allocated`}</Text> | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (available != quantity) { | ||||
|           if (available > 0) { | ||||
|             extra.push( | ||||
|               <Text size="sm" color="orange"> | ||||
|                 {t`Available` + `: ${available}`} | ||||
|               </Text> | ||||
|             ); | ||||
|           } else { | ||||
|             extra.push( | ||||
|               <Text size="sm" color="red">{t`No stock available`}</Text> | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (quantity <= 0) { | ||||
|           color = 'red'; | ||||
|           extra.push( | ||||
|             <Text size="sm">{t`This stock item has been depleted`}</Text> | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return ( | ||||
|           <TableHoverCard | ||||
|             value={ | ||||
|               <Group spacing="xs" position="left"> | ||||
|                 <Text color={color}>{text}</Text> | ||||
|                 {part.units && ( | ||||
|                   <Text size="xs" color={color}> | ||||
|                     [{part.units}] | ||||
|                   </Text> | ||||
|                 )} | ||||
|               </Group> | ||||
|             } | ||||
|             title={t`Stock Information`} | ||||
|             extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>} | ||||
|           /> | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'status', | ||||
|   | ||||
| @@ -94,8 +94,8 @@ export function editPart({ | ||||
|     title: t`Edit Part`, | ||||
|     url: ApiPaths.part_list, | ||||
|     pk: part_id, | ||||
|     successMessage: t`Part updated`, | ||||
|     fields: partFields({ editing: true }), | ||||
|     successMessage: t`Part updated`, | ||||
|     onFormSuccess: callback | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -11,12 +11,16 @@ import { openCreateApiForm, openEditApiForm } from '../forms'; | ||||
| /** | ||||
|  * Construct a set of fields for creating / editing a StockItem instance | ||||
|  */ | ||||
| export function stockFields({}: {}): ApiFormFieldSet { | ||||
| export function stockFields({ | ||||
|   create = false | ||||
| }: { | ||||
|   create: boolean; | ||||
| }): ApiFormFieldSet { | ||||
|   let fields: ApiFormFieldSet = { | ||||
|     part: { | ||||
|       hidden: !create, | ||||
|       onValueChange: (change: ApiFormChangeCallback) => { | ||||
|         // TODO: implement remaining functionality from old stock.py | ||||
|         console.log('part changed: ', change.value); | ||||
|  | ||||
|         // Clear the 'supplier_part' field if the part is changed | ||||
|         change.form.setValues({ | ||||
| @@ -41,15 +45,18 @@ export function stockFields({}: {}): ApiFormFieldSet { | ||||
|       } | ||||
|     }, | ||||
|     use_pack_size: { | ||||
|       hidden: !create, | ||||
|       description: t`Add given quantity as packs instead of individual items` | ||||
|     }, | ||||
|     location: { | ||||
|       hidden: !create, | ||||
|       filters: { | ||||
|         structural: false | ||||
|       } | ||||
|       // TODO: icon | ||||
|     }, | ||||
|     quantity: { | ||||
|       hidden: !create, | ||||
|       description: t`Enter initial quantity for this stock item` | ||||
|     }, | ||||
|     serial_numbers: { | ||||
| @@ -57,9 +64,11 @@ export function stockFields({}: {}): ApiFormFieldSet { | ||||
|       field_type: 'string', | ||||
|       label: t`Serial Numbers`, | ||||
|       description: t`Enter serial numbers for new stock (or leave blank)`, | ||||
|       required: false | ||||
|       required: false, | ||||
|       hidden: !create | ||||
|     }, | ||||
|     serial: { | ||||
|       hidden: create | ||||
|       // TODO: icon | ||||
|     }, | ||||
|     batch: { | ||||
| @@ -100,7 +109,7 @@ export function createStockItem() { | ||||
|   openCreateApiForm({ | ||||
|     name: 'stockitem-create', | ||||
|     url: ApiPaths.stock_item_list, | ||||
|     fields: stockFields({}), | ||||
|     fields: stockFields({ create: true }), | ||||
|     title: t`Create Stock Item` | ||||
|   }); | ||||
| } | ||||
| @@ -109,12 +118,20 @@ export function createStockItem() { | ||||
|  * Launch a form to edit an existing StockItem instance | ||||
|  * @param item : primary key of the StockItem to edit | ||||
|  */ | ||||
| export function editStockItem(item: number) { | ||||
| export function editStockItem({ | ||||
|   item_id, | ||||
|   callback | ||||
| }: { | ||||
|   item_id: number; | ||||
|   callback?: () => void; | ||||
| }) { | ||||
|   openEditApiForm({ | ||||
|     name: 'stockitem-edit', | ||||
|     url: ApiPaths.stock_item_list, | ||||
|     pk: item, | ||||
|     fields: stockFields({}), | ||||
|     title: t`Edit Stock Item` | ||||
|     pk: item_id, | ||||
|     fields: stockFields({ create: false }), | ||||
|     title: t`Edit Stock Item`, | ||||
|     successMessage: t`Stock item updated`, | ||||
|     onFormSuccess: callback | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,10 @@ import { | ||||
| import { useMemo, useState } from 'react'; | ||||
| import { useParams } from 'react-router-dom'; | ||||
|  | ||||
| import { ActionDropdown } from '../../components/items/ActionDropdown'; | ||||
| import { | ||||
|   ActionDropdown, | ||||
|   BarcodeActionDropdown | ||||
| } from '../../components/items/ActionDropdown'; | ||||
| import { PageDetail } from '../../components/nav/PageDetail'; | ||||
| import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; | ||||
| import { PartCategoryTree } from '../../components/nav/PartCategoryTree'; | ||||
| @@ -236,10 +239,7 @@ export default function PartDetail() { | ||||
|   const partActions = useMemo(() => { | ||||
|     // TODO: Disable actions based on user permissions | ||||
|     return [ | ||||
|       <ActionDropdown | ||||
|         key="barcode" | ||||
|         tooltip={t`Barcode Actions`} | ||||
|         icon={<IconQrcode />} | ||||
|       <BarcodeActionDropdown | ||||
|         actions={[ | ||||
|           { | ||||
|             icon: <IconQrcode />, | ||||
|   | ||||
| @@ -3,27 +3,48 @@ import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; | ||||
| import { | ||||
|   IconBookmark, | ||||
|   IconBoxPadding, | ||||
|   IconChecklist, | ||||
|   IconCircleCheck, | ||||
|   IconCircleMinus, | ||||
|   IconCirclePlus, | ||||
|   IconCopy, | ||||
|   IconDots, | ||||
|   IconEdit, | ||||
|   IconHistory, | ||||
|   IconInfoCircle, | ||||
|   IconLink, | ||||
|   IconNotes, | ||||
|   IconPackages, | ||||
|   IconPaperclip, | ||||
|   IconSitemap | ||||
|   IconQrcode, | ||||
|   IconSitemap, | ||||
|   IconTransfer, | ||||
|   IconTrash, | ||||
|   IconUnlink | ||||
| } from '@tabler/icons-react'; | ||||
| import { useMemo, useState } from 'react'; | ||||
| import { useParams } from 'react-router-dom'; | ||||
|  | ||||
| import { | ||||
|   ActionDropdown, | ||||
|   BarcodeActionDropdown | ||||
| } from '../../components/items/ActionDropdown'; | ||||
| import { PlaceholderPanel } from '../../components/items/Placeholder'; | ||||
| import { PageDetail } from '../../components/nav/PageDetail'; | ||||
| import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; | ||||
| import { StockLocationTree } from '../../components/nav/StockLocationTree'; | ||||
| import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; | ||||
| import { NotesEditor } from '../../components/widgets/MarkdownEditor'; | ||||
| import { editStockItem } from '../../functions/forms/StockForms'; | ||||
| import { useInstance } from '../../hooks/UseInstance'; | ||||
| import { ApiPaths, apiUrl } from '../../states/ApiState'; | ||||
| import { useUserState } from '../../states/UserState'; | ||||
|  | ||||
| export default function StockDetail() { | ||||
|   const { id } = useParams(); | ||||
|  | ||||
|   const user = useUserState(); | ||||
|  | ||||
|   const [treeOpen, setTreeOpen] = useState(false); | ||||
|  | ||||
|   const { | ||||
| @@ -58,13 +79,22 @@ export default function StockDetail() { | ||||
|         name: 'allocations', | ||||
|         label: t`Allocations`, | ||||
|         icon: <IconBookmark />, | ||||
|         content: <PlaceholderPanel /> | ||||
|         content: <PlaceholderPanel />, | ||||
|         hidden: | ||||
|           !stockitem?.part_detail?.salable && !stockitem?.part_detail?.component | ||||
|       }, | ||||
|       { | ||||
|         name: 'testdata', | ||||
|         label: t`Test Data`, | ||||
|         icon: <IconChecklist />, | ||||
|         hidden: !stockitem?.part_detail?.trackable | ||||
|       }, | ||||
|       { | ||||
|         name: 'installed_items', | ||||
|         label: t`Installed Items`, | ||||
|         icon: <IconBoxPadding />, | ||||
|         content: <PlaceholderPanel /> | ||||
|         content: <PlaceholderPanel />, | ||||
|         hidden: !stockitem?.part_detail?.assembly | ||||
|       }, | ||||
|       { | ||||
|         name: 'child_items', | ||||
| @@ -110,6 +140,89 @@ export default function StockDetail() { | ||||
|     [stockitem] | ||||
|   ); | ||||
|  | ||||
|   const stockActions = useMemo( | ||||
|     () => /* TODO: Disable actions based on user permissions*/ [ | ||||
|       <BarcodeActionDropdown | ||||
|         actions={[ | ||||
|           { | ||||
|             icon: <IconQrcode />, | ||||
|             name: t`View`, | ||||
|             tooltip: t`View part barcode` | ||||
|           }, | ||||
|           { | ||||
|             icon: <IconLink />, | ||||
|             name: t`Link Barcode`, | ||||
|             tooltip: t`Link custom barcode to stock item`, | ||||
|             disabled: stockitem?.barcode_hash | ||||
|           }, | ||||
|           { | ||||
|             icon: <IconUnlink />, | ||||
|             name: t`Unlink Barcode`, | ||||
|             tooltip: t`Unlink custom barcode from stock item`, | ||||
|             disabled: !stockitem?.barcode_hash | ||||
|           } | ||||
|         ]} | ||||
|       />, | ||||
|       <ActionDropdown | ||||
|         key="operations" | ||||
|         tooltip={t`Stock Operations`} | ||||
|         icon={<IconPackages />} | ||||
|         actions={[ | ||||
|           { | ||||
|             name: t`Count`, | ||||
|             tooltip: t`Count stock`, | ||||
|             icon: <IconCircleCheck color="green" /> | ||||
|           }, | ||||
|           { | ||||
|             name: t`Add`, | ||||
|             tooltip: t`Add stock`, | ||||
|             icon: <IconCirclePlus color="green" /> | ||||
|           }, | ||||
|           { | ||||
|             name: t`Remove`, | ||||
|             tooltip: t`Remove stock`, | ||||
|             icon: <IconCircleMinus color="red" /> | ||||
|           }, | ||||
|           { | ||||
|             name: t`Transfer`, | ||||
|             tooltip: t`Transfer stock`, | ||||
|             icon: <IconTransfer color="blue" /> | ||||
|           } | ||||
|         ]} | ||||
|       />, | ||||
|       <ActionDropdown | ||||
|         key="stock" | ||||
|         // tooltip={t`Stock Actions`} | ||||
|         icon={<IconDots />} | ||||
|         actions={[ | ||||
|           { | ||||
|             name: t`Duplicate`, | ||||
|             tooltip: t`Duplicate stock item`, | ||||
|             icon: <IconCopy /> | ||||
|           }, | ||||
|           { | ||||
|             name: t`Edit`, | ||||
|             tooltip: t`Edit stock item`, | ||||
|             icon: <IconEdit color="blue" />, | ||||
|             onClick: () => { | ||||
|               stockitem.pk && | ||||
|                 editStockItem({ | ||||
|                   item_id: stockitem.pk, | ||||
|                   callback: () => refreshInstance | ||||
|                 }); | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             name: t`Delete`, | ||||
|             tooltip: t`Delete stock item`, | ||||
|             icon: <IconTrash color="red" /> | ||||
|           } | ||||
|         ]} | ||||
|       /> | ||||
|     ], | ||||
|     [id, stockitem, user] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Stack> | ||||
|       <LoadingOverlay visible={instanceQuery.isFetching} /> | ||||
| @@ -119,8 +232,9 @@ export default function StockDetail() { | ||||
|         selectedLocation={stockitem?.location} | ||||
|       /> | ||||
|       <PageDetail | ||||
|         title={t`Stock Items`} | ||||
|         subtitle={stockitem.part_detail?.full_name ?? 'name goes here'} | ||||
|         title={t`Stock Item`} | ||||
|         subtitle={stockitem.part_detail?.full_name} | ||||
|         imageUrl={stockitem.part_detail?.thumbnail} | ||||
|         detail={ | ||||
|           <Alert color="teal" title="Stock Item"> | ||||
|             <Text>Quantity: {stockitem.quantity ?? 'idk'}</Text> | ||||
| @@ -130,6 +244,7 @@ export default function StockDetail() { | ||||
|         breadcrumbAction={() => { | ||||
|           setTreeOpen(true); | ||||
|         }} | ||||
|         actions={stockActions} | ||||
|       /> | ||||
|       <PanelGroup pageKey="stockitem" panels={stockPanels} /> | ||||
|     </Stack> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user