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:
Oliver 2023-11-01 07:32:40 +11:00 committed by GitHub
parent 2908ad0721
commit e18b6d38ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 311 additions and 36 deletions

View File

@ -1,4 +1,6 @@
import { t } from '@lingui/macro';
import { ActionIcon, Menu, Tooltip } from '@mantine/core'; import { ActionIcon, Menu, Tooltip } from '@mantine/core';
import { IconQrcode } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
@ -32,7 +34,7 @@ export function ActionDropdown({
return hasActions ? ( return hasActions ? (
<Menu position="bottom-end"> <Menu position="bottom-end">
<Menu.Target> <Menu.Target>
<Tooltip label={tooltip}> <Tooltip label={tooltip} hidden={!tooltip}>
<ActionIcon size="lg" radius="sm" variant="outline"> <ActionIcon size="lg" radius="sm" variant="outline">
{icon} {icon}
</ActionIcon> </ActionIcon>
@ -63,3 +65,19 @@ export function ActionDropdown({
</Menu> </Menu>
) : null; ) : null;
} }
// Dropdown menu for barcode actions
export function BarcodeActionDropdown({
actions
}: {
actions: ActionDropdownItem[];
}) {
return (
<ActionDropdown
key="barcode"
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={actions}
/>
);
}

View File

@ -75,16 +75,19 @@ function partTableColumns(): TableColumn[] {
let extra: ReactNode[] = []; let extra: ReactNode[] = [];
let stock = record?.total_in_stock ?? 0; 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 text = String(stock);
let color: string | undefined = undefined; let color: string | undefined = undefined;
if (record.minimum_stock > stock) { if (min_stock > stock) {
extra.push( extra.push(
<Text color="orange"> <Text color="orange">{t`Minimum stock` + `: ${min_stock}`}</Text>
{t`Minimum stock` + `: ${record.minimum_stock}`}
</Text>
); );
color = 'orange'; 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'; color = 'red';
text = t`No stock`; text = t`No stock`;
} else if (available <= 0) {
color = 'orange';
} else if (available < min_stock) {
color = 'yellow';
} }
return ( return (
@ -129,7 +140,7 @@ function partTableColumns(): TableColumn[] {
<Group spacing="xs" position="left"> <Group spacing="xs" position="left">
<Text color={color}>{text}</Text> <Text color={color}>{text}</Text>
{record.units && ( {record.units && (
<Text size="xs" color="color"> <Text size="xs" color={color}>
[{record.units}] [{record.units}]
</Text> </Text>
)} )}

View File

@ -61,7 +61,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
accessor: 'project_code', accessor: 'project_code',
title: t`Project Code`, title: t`Project Code`,
switchable: true switchable: true
// TODO: Custom formatter // TODO: Custom project code formatter
}, },
{ {
accessor: 'status', accessor: 'status',
@ -78,22 +78,34 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
accessor: 'creation_date', accessor: 'creation_date',
title: t`Created`, title: t`Created`,
switchable: true switchable: true
// TODO: Custom formatter // TODO: Custom date formatter
}, },
{ {
accessor: 'target_date', accessor: 'target_date',
title: t`Target Date`, title: t`Target Date`,
switchable: true switchable: true
// TODO: Custom formatter // TODO: Custom date formatter
}, },
{ {
accessor: 'line_items', accessor: 'line_items',
title: t`Line Items`, title: t`Line Items`,
sortable: true, sortable: true,
switchable: 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
]; ];
}, []); }, []);

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core'; import { Group, Stack, Text } from '@mantine/core';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications'; import { notYetImplemented } from '../../../functions/notifications';
@ -12,6 +12,7 @@ import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
import { RowAction } from '../RowActions'; import { RowAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
import { InvenTreeTable } from './../InvenTreeTable'; import { InvenTreeTable } from './../InvenTreeTable';
/** /**
@ -46,8 +47,109 @@ function stockItemTableColumns(): TableColumn[] {
{ {
accessor: 'quantity', accessor: 'quantity',
sortable: true, sortable: true,
title: t`Stock` title: t`Stock`,
// TODO: Custom renderer for stock quantity 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', accessor: 'status',

View File

@ -94,8 +94,8 @@ export function editPart({
title: t`Edit Part`, title: t`Edit Part`,
url: ApiPaths.part_list, url: ApiPaths.part_list,
pk: part_id, pk: part_id,
successMessage: t`Part updated`,
fields: partFields({ editing: true }), fields: partFields({ editing: true }),
successMessage: t`Part updated`,
onFormSuccess: callback onFormSuccess: callback
}); });
} }

View File

@ -11,12 +11,16 @@ import { openCreateApiForm, openEditApiForm } from '../forms';
/** /**
* Construct a set of fields for creating / editing a StockItem instance * 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 = { let fields: ApiFormFieldSet = {
part: { part: {
hidden: !create,
onValueChange: (change: ApiFormChangeCallback) => { onValueChange: (change: ApiFormChangeCallback) => {
// TODO: implement remaining functionality from old stock.py // TODO: implement remaining functionality from old stock.py
console.log('part changed: ', change.value);
// Clear the 'supplier_part' field if the part is changed // Clear the 'supplier_part' field if the part is changed
change.form.setValues({ change.form.setValues({
@ -41,15 +45,18 @@ export function stockFields({}: {}): ApiFormFieldSet {
} }
}, },
use_pack_size: { use_pack_size: {
hidden: !create,
description: t`Add given quantity as packs instead of individual items` description: t`Add given quantity as packs instead of individual items`
}, },
location: { location: {
hidden: !create,
filters: { filters: {
structural: false structural: false
} }
// TODO: icon // TODO: icon
}, },
quantity: { quantity: {
hidden: !create,
description: t`Enter initial quantity for this stock item` description: t`Enter initial quantity for this stock item`
}, },
serial_numbers: { serial_numbers: {
@ -57,9 +64,11 @@ export function stockFields({}: {}): ApiFormFieldSet {
field_type: 'string', field_type: 'string',
label: t`Serial Numbers`, label: t`Serial Numbers`,
description: t`Enter serial numbers for new stock (or leave blank)`, description: t`Enter serial numbers for new stock (or leave blank)`,
required: false required: false,
hidden: !create
}, },
serial: { serial: {
hidden: create
// TODO: icon // TODO: icon
}, },
batch: { batch: {
@ -100,7 +109,7 @@ export function createStockItem() {
openCreateApiForm({ openCreateApiForm({
name: 'stockitem-create', name: 'stockitem-create',
url: ApiPaths.stock_item_list, url: ApiPaths.stock_item_list,
fields: stockFields({}), fields: stockFields({ create: true }),
title: t`Create Stock Item` title: t`Create Stock Item`
}); });
} }
@ -109,12 +118,20 @@ export function createStockItem() {
* Launch a form to edit an existing StockItem instance * Launch a form to edit an existing StockItem instance
* @param item : primary key of the StockItem to edit * @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({ openEditApiForm({
name: 'stockitem-edit', name: 'stockitem-edit',
url: ApiPaths.stock_item_list, url: ApiPaths.stock_item_list,
pk: item, pk: item_id,
fields: stockFields({}), fields: stockFields({ create: false }),
title: t`Edit Stock Item` title: t`Edit Stock Item`,
successMessage: t`Stock item updated`,
onFormSuccess: callback
}); });
} }

View File

@ -30,7 +30,10 @@ import {
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; 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 { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartCategoryTree } from '../../components/nav/PartCategoryTree'; import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
@ -236,10 +239,7 @@ export default function PartDetail() {
const partActions = useMemo(() => { const partActions = useMemo(() => {
// TODO: Disable actions based on user permissions // TODO: Disable actions based on user permissions
return [ return [
<ActionDropdown <BarcodeActionDropdown
key="barcode"
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={[ actions={[
{ {
icon: <IconQrcode />, icon: <IconQrcode />,

View File

@ -3,27 +3,48 @@ import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
import { import {
IconBookmark, IconBookmark,
IconBoxPadding, IconBoxPadding,
IconChecklist,
IconCircleCheck,
IconCircleMinus,
IconCirclePlus,
IconCopy,
IconDots,
IconEdit,
IconHistory, IconHistory,
IconInfoCircle, IconInfoCircle,
IconLink,
IconNotes, IconNotes,
IconPackages,
IconPaperclip, IconPaperclip,
IconSitemap IconQrcode,
IconSitemap,
IconTransfer,
IconTrash,
IconUnlink
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import {
ActionDropdown,
BarcodeActionDropdown
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { StockLocationTree } from '../../components/nav/StockLocationTree';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editStockItem } from '../../functions/forms/StockForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
export default function StockDetail() { export default function StockDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -58,13 +79,22 @@ export default function StockDetail() {
name: 'allocations', name: 'allocations',
label: t`Allocations`, label: t`Allocations`,
icon: <IconBookmark />, 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', name: 'installed_items',
label: t`Installed Items`, label: t`Installed Items`,
icon: <IconBoxPadding />, icon: <IconBoxPadding />,
content: <PlaceholderPanel /> content: <PlaceholderPanel />,
hidden: !stockitem?.part_detail?.assembly
}, },
{ {
name: 'child_items', name: 'child_items',
@ -110,6 +140,89 @@ export default function StockDetail() {
[stockitem] [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 ( return (
<Stack> <Stack>
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
@ -119,8 +232,9 @@ export default function StockDetail() {
selectedLocation={stockitem?.location} selectedLocation={stockitem?.location}
/> />
<PageDetail <PageDetail
title={t`Stock Items`} title={t`Stock Item`}
subtitle={stockitem.part_detail?.full_name ?? 'name goes here'} subtitle={stockitem.part_detail?.full_name}
imageUrl={stockitem.part_detail?.thumbnail}
detail={ detail={
<Alert color="teal" title="Stock Item"> <Alert color="teal" title="Stock Item">
<Text>Quantity: {stockitem.quantity ?? 'idk'}</Text> <Text>Quantity: {stockitem.quantity ?? 'idk'}</Text>
@ -130,6 +244,7 @@ export default function StockDetail() {
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);
}} }}
actions={stockActions}
/> />
<PanelGroup pageKey="stockitem" panels={stockPanels} /> <PanelGroup pageKey="stockitem" panels={stockPanels} />
</Stack> </Stack>