mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
2908ad0721
commit
e18b6d38ef
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user