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 { 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}
/>
);
}

View File

@ -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>
)}

View File

@ -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
];
}, []);

View File

@ -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',

View File

@ -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
});
}

View File

@ -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
});
}

View File

@ -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 />,

View File

@ -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>