mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[React] BOM table (#5816)
* Add BuildOrderTable to part page * Add UsedInTable * Display SalesOrderTable on PartDetail page * Fix horizontal overflow for panel group * First pass at BomTable * Adds <TableHoverCard> component - Allows us to display more information in a table cell, without clutter * Fix for row actions - Prevent opening row actions menu from "clicking" the row - Prevent selection of row action from "clicking" the row * Further work on BOM table - Column rendering - Placeholder actions * Prevent navigation to a panel group page and selection of invalid panel * Fix unused references
This commit is contained in:
parent
98d7c49ea5
commit
3f7d05339b
@ -54,11 +54,16 @@ export function PanelGroup({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update the active panel when the selected panel changes
|
// Update the active panel when the selected panel changes
|
||||||
|
// If the selected panel is not available, default to the first available panel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPanel) {
|
let activePanelNames = panels
|
||||||
setActivePanel(selectedPanel);
|
.filter((panel) => !panel.hidden && !panel.disabled)
|
||||||
|
.map((panel) => panel.name);
|
||||||
|
|
||||||
|
if (!activePanelNames.includes(activePanel)) {
|
||||||
|
setActivePanel(activePanelNames.length > 0 ? activePanelNames[0] : '');
|
||||||
}
|
}
|
||||||
}, [selectedPanel]);
|
}, [panels]);
|
||||||
|
|
||||||
// Callback when the active panel changes
|
// Callback when the active panel changes
|
||||||
function handlePanelChange(panel: string) {
|
function handlePanelChange(panel: string) {
|
||||||
@ -116,7 +121,15 @@ export function PanelGroup({
|
|||||||
{panels.map(
|
{panels.map(
|
||||||
(panel, idx) =>
|
(panel, idx) =>
|
||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tabs.Panel key={idx} value={panel.name} p="sm">
|
<Tabs.Panel
|
||||||
|
key={idx}
|
||||||
|
value={panel.name}
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
overflowX: 'scroll',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
<StylishText size="lg">{panel.label}</StylishText>
|
<StylishText size="lg">{panel.label}</StylishText>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -11,7 +11,7 @@ export function TableColumnSelect({
|
|||||||
onToggleColumn: (columnName: string) => void;
|
onToggleColumn: (columnName: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Menu shadow="xs">
|
<Menu shadow="xs" closeOnItemClick={false}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<Tooltip label={t`Select Columns`}>
|
<Tooltip label={t`Select Columns`}>
|
||||||
|
@ -421,7 +421,7 @@ export function InvenTreeTable({
|
|||||||
onCreateFilter={onFilterAdd}
|
onCreateFilter={onFilterAdd}
|
||||||
onClose={() => setFilterSelectOpen(false)}
|
onClose={() => setFilterSelectOpen(false)}
|
||||||
/>
|
/>
|
||||||
<Stack>
|
<Stack spacing="sm">
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group position="left" key="custom-actions" spacing={5}>
|
<Group position="left" key="custom-actions" spacing={5}>
|
||||||
{tableProps.customActionGroups?.map(
|
{tableProps.customActionGroups?.map(
|
||||||
@ -522,6 +522,10 @@ export function InvenTreeTable({
|
|||||||
records={data}
|
records={data}
|
||||||
columns={dataColumns}
|
columns={dataColumns}
|
||||||
onRowClick={tableProps.onRowClick}
|
onRowClick={tableProps.onRowClick}
|
||||||
|
defaultColumnProps={{
|
||||||
|
noWrap: true,
|
||||||
|
textAlignment: 'left'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
@ -2,13 +2,15 @@ import { t } from '@lingui/macro';
|
|||||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { Menu, Text } from '@mantine/core';
|
import { Menu, Text } from '@mantine/core';
|
||||||
import { IconDots } from '@tabler/icons-react';
|
import { IconDots } from '@tabler/icons-react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
import { notYetImplemented } from '../../functions/notifications';
|
||||||
|
|
||||||
// Type definition for a table row action
|
// Type definition for a table row action
|
||||||
export type RowAction = {
|
export type RowAction = {
|
||||||
title: string;
|
title: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
};
|
};
|
||||||
@ -26,12 +28,33 @@ export function RowActions({
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
actions: RowAction[];
|
actions: RowAction[];
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
|
// Prevent default event handling
|
||||||
|
// Ref: https://icflorescu.github.io/mantine-datatable/examples/links-or-buttons-inside-clickable-rows-or-cells
|
||||||
|
function openMenu(event: any) {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
actions.length > 0 && (
|
actions.length > 0 && (
|
||||||
<Menu withinPortal={true} disabled={disabled}>
|
<Menu
|
||||||
|
withinPortal={true}
|
||||||
|
disabled={disabled}
|
||||||
|
opened={opened}
|
||||||
|
onChange={setOpened}
|
||||||
|
>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Tooltip label={title || t`Actions`}>
|
<Tooltip label={title || t`Actions`}>
|
||||||
<ActionIcon disabled={disabled} variant="subtle" color="gray">
|
<ActionIcon
|
||||||
|
onClick={openMenu}
|
||||||
|
disabled={disabled}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -41,7 +64,17 @@ export function RowActions({
|
|||||||
{actions.map((action, idx) => (
|
{actions.map((action, idx) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={action.onClick}
|
onClick={(event) => {
|
||||||
|
// Prevent clicking on the action from selecting the row itself
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
|
if (action.onClick) {
|
||||||
|
action.onClick();
|
||||||
|
} else {
|
||||||
|
notYetImplemented();
|
||||||
|
}
|
||||||
|
}}
|
||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
title={action.tooltip || action.title}
|
title={action.tooltip || action.title}
|
||||||
>
|
>
|
||||||
|
43
src/frontend/src/components/tables/TableHoverCard.tsx
Normal file
43
src/frontend/src/components/tables/TableHoverCard.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Divider, Group, HoverCard, Stack } from '@mantine/core';
|
||||||
|
import { IconInfoCircle } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A custom hovercard element for displaying extra information in a table cell.
|
||||||
|
* If a table cell has extra information available,
|
||||||
|
* it can be displayed as a drop-down hovercard when the user hovers over the cell.
|
||||||
|
*/
|
||||||
|
export function TableHoverCard({
|
||||||
|
value, // The value of the cell
|
||||||
|
extra, // The extra information to display
|
||||||
|
title // The title of the hovercard
|
||||||
|
}: {
|
||||||
|
value: any;
|
||||||
|
extra?: any;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
|
// If no extra information presented, just return the raw value
|
||||||
|
if (!extra) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCard.Target>
|
||||||
|
<Group spacing="xs" position="apart">
|
||||||
|
{value}
|
||||||
|
<IconInfoCircle size="16" color="blue" />
|
||||||
|
</Group>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Group spacing="xs" position="left">
|
||||||
|
<IconInfoCircle size="16" color="blue" />
|
||||||
|
{title}
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
{extra}
|
||||||
|
</Stack>
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
239
src/frontend/src/components/tables/bom/BomTable.tsx
Normal file
239
src/frontend/src/components/tables/bom/BomTable.tsx
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Stack, Text } from '@mantine/core';
|
||||||
|
import { ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
|
import { useUserState } from '../../../states/UserState';
|
||||||
|
import { ThumbnailHoverCard } from '../../images/Thumbnail';
|
||||||
|
import { YesNoButton } from '../../items/YesNoButton';
|
||||||
|
import { TableColumn } from '../Column';
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
import { RowAction } from '../RowActions';
|
||||||
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
||||||
|
export function BomTable({
|
||||||
|
partId,
|
||||||
|
params = {}
|
||||||
|
}: {
|
||||||
|
partId: number;
|
||||||
|
params?: any;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const user = useUserState();
|
||||||
|
|
||||||
|
const { tableKey } = useTableRefresh('bom');
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
// TODO: Improve column rendering
|
||||||
|
{
|
||||||
|
accessor: 'part',
|
||||||
|
title: t`Part`,
|
||||||
|
render: (row) => {
|
||||||
|
let part = row.sub_part_detail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
part && (
|
||||||
|
<ThumbnailHoverCard
|
||||||
|
src={part.thumbnail || part.image}
|
||||||
|
text={part.full_name}
|
||||||
|
alt={part.description}
|
||||||
|
link=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
title: t`Description`,
|
||||||
|
switchable: true,
|
||||||
|
render: (row) => row?.sub_part_detail?.description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'reference',
|
||||||
|
switchable: true,
|
||||||
|
title: t`Reference`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'quantity',
|
||||||
|
title: t`Quantity`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'substitutes',
|
||||||
|
title: t`Substitutes`,
|
||||||
|
switchable: true,
|
||||||
|
render: (row) => {
|
||||||
|
let substitutes = row.substitutes ?? [];
|
||||||
|
|
||||||
|
return substitutes.length > 0 ? (
|
||||||
|
row.length
|
||||||
|
) : (
|
||||||
|
<YesNoButton value={false} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'optional',
|
||||||
|
title: t`Optional`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (row) => {
|
||||||
|
return <YesNoButton value={row.optional} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'consumable',
|
||||||
|
title: t`Consumable`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (row) => {
|
||||||
|
return <YesNoButton value={row.consumable} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'allow_variants',
|
||||||
|
title: t`Allow Variants`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (row) => {
|
||||||
|
return <YesNoButton value={row.allow_variants} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'inherited',
|
||||||
|
title: t`Gets Inherited`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (row) => {
|
||||||
|
// TODO: Update complexity here
|
||||||
|
return <YesNoButton value={row.inherited} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'price_range',
|
||||||
|
title: t`Price Range`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
render: (row) => {
|
||||||
|
let min_price = row.pricing_min || row.pricing_max;
|
||||||
|
let max_price = row.pricing_max || row.pricing_min;
|
||||||
|
|
||||||
|
// TODO: Custom price range rendering component
|
||||||
|
return `${min_price} - ${max_price}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'available_stock',
|
||||||
|
title: t`Available`,
|
||||||
|
switchable: true,
|
||||||
|
render: (row) => {
|
||||||
|
let extra: ReactNode[] = [];
|
||||||
|
|
||||||
|
let available_stock: number = row?.available_stock ?? 0;
|
||||||
|
let substitute_stock: number = row?.substitute_stock ?? 0;
|
||||||
|
let variant_stock: number = row?.variant_stock ?? 0;
|
||||||
|
let on_order: number = row?.on_order ?? 0;
|
||||||
|
|
||||||
|
if (available_stock <= 0) {
|
||||||
|
return <Text color="red" italic>{t`No stock`}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substitute_stock > 0) {
|
||||||
|
extra.push(
|
||||||
|
<Text key="substitute">{t`Includes substitute stock`}</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant_stock > 0) {
|
||||||
|
extra.push(<Text key="variant">{t`Includes variant stock`}</Text>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (on_order > 0) {
|
||||||
|
extra.push(
|
||||||
|
<Text key="on_order">
|
||||||
|
{t`On order`}: {on_order}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHoverCard
|
||||||
|
value={available_stock}
|
||||||
|
extra={
|
||||||
|
extra.length > 0 ? <Stack spacing="xs">{extra}</Stack> : null
|
||||||
|
}
|
||||||
|
title={t`Available Stock`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'can_build',
|
||||||
|
title: t`Can Build`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true // TODO: Custom sorting via API
|
||||||
|
// TODO: Reference bom.js for canBuildQuantity method
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'note',
|
||||||
|
title: t`Notes`,
|
||||||
|
switchable: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [partId, params]);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
return [];
|
||||||
|
}, [partId, params]);
|
||||||
|
|
||||||
|
const rowActions = useCallback(
|
||||||
|
(record: any) => {
|
||||||
|
// TODO: Check user permissions here,
|
||||||
|
// TODO: to determine which actions are allowed
|
||||||
|
|
||||||
|
let actions: RowAction[] = [];
|
||||||
|
|
||||||
|
if (!record.validated) {
|
||||||
|
actions.push({
|
||||||
|
title: t`Validate`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
title: t`Edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
title: t`Delete`,
|
||||||
|
color: 'red'
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
},
|
||||||
|
[partId, user]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiPaths.bom_list)}
|
||||||
|
tableKey={tableKey}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
part: partId,
|
||||||
|
part_detail: true,
|
||||||
|
sub_part_detail: true
|
||||||
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
|
onRowClick: (row) => navigate(`/part/${row.sub_part}`),
|
||||||
|
rowActions: rowActions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
103
src/frontend/src/components/tables/bom/UsedInTable.tsx
Normal file
103
src/frontend/src/components/tables/bom/UsedInTable.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
|
import { ThumbnailHoverCard } from '../../images/Thumbnail';
|
||||||
|
import { TableColumn } from '../Column';
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For a given part, render a table showing all the assemblies the part is used in
|
||||||
|
*/
|
||||||
|
export function UsedInTable({
|
||||||
|
partId,
|
||||||
|
params = {}
|
||||||
|
}: {
|
||||||
|
partId: number;
|
||||||
|
params?: any;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { tableKey } = useTableRefresh('usedin');
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: 'part',
|
||||||
|
title: t`Assembled Part`,
|
||||||
|
switchable: false,
|
||||||
|
sortable: true,
|
||||||
|
render: (record: any) => {
|
||||||
|
let part = record.part_detail;
|
||||||
|
return (
|
||||||
|
part && (
|
||||||
|
<ThumbnailHoverCard
|
||||||
|
src={part.thumbnail || part.image}
|
||||||
|
text={part.full_name}
|
||||||
|
alt={part.description}
|
||||||
|
link=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'sub_part',
|
||||||
|
title: t`Required Part`,
|
||||||
|
sortable: true,
|
||||||
|
render: (record: any) => {
|
||||||
|
let part = record.sub_part_detail;
|
||||||
|
return (
|
||||||
|
part && (
|
||||||
|
<ThumbnailHoverCard
|
||||||
|
src={part.thumbnail || part.image}
|
||||||
|
text={part.full_name}
|
||||||
|
alt={part.description}
|
||||||
|
link=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'quantity',
|
||||||
|
title: t`Quantity`,
|
||||||
|
render: (record: any) => {
|
||||||
|
// TODO: render units if appropriate
|
||||||
|
return record.quantity;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'reference',
|
||||||
|
title: t`Reference`,
|
||||||
|
sortable: true,
|
||||||
|
switchable: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [partId]);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
return [];
|
||||||
|
}, [partId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiPaths.bom_list)}
|
||||||
|
tableKey={tableKey}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
uses: partId,
|
||||||
|
part_detail: true,
|
||||||
|
sub_part_detail: true
|
||||||
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
|
onRowClick: (row) => navigate(`/part/${row.part}`)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -34,10 +34,14 @@ import { ActionDropdown } 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';
|
||||||
|
import { BomTable } from '../../components/tables/bom/BomTable';
|
||||||
|
import { UsedInTable } from '../../components/tables/bom/UsedInTable';
|
||||||
|
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
|
||||||
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
||||||
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
|
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
|
||||||
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
|
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
|
||||||
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
||||||
|
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
|
||||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
import { editPart } from '../../functions/forms/PartForms';
|
import { editPart } from '../../functions/forms/PartForms';
|
||||||
@ -105,19 +109,29 @@ export default function PartDetail() {
|
|||||||
name: 'bom',
|
name: 'bom',
|
||||||
label: t`Bill of Materials`,
|
label: t`Bill of Materials`,
|
||||||
icon: <IconListTree />,
|
icon: <IconListTree />,
|
||||||
hidden: !part.assembly
|
hidden: !part.assembly,
|
||||||
|
content: <BomTable partId={part.pk ?? -1} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'builds',
|
name: 'builds',
|
||||||
label: t`Build Orders`,
|
label: t`Build Orders`,
|
||||||
icon: <IconTools />,
|
icon: <IconTools />,
|
||||||
hidden: !part.assembly && !part.component
|
hidden: !part.assembly && !part.component,
|
||||||
|
content: (
|
||||||
|
<BuildOrderTable
|
||||||
|
params={{
|
||||||
|
part_detail: true,
|
||||||
|
part: part.pk ?? -1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'used_in',
|
name: 'used_in',
|
||||||
label: t`Used In`,
|
label: t`Used In`,
|
||||||
icon: <IconStack2 />,
|
icon: <IconStack2 />,
|
||||||
hidden: !part.component
|
hidden: !part.component,
|
||||||
|
content: <UsedInTable partId={part.pk ?? -1} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'pricing',
|
name: 'pricing',
|
||||||
@ -140,7 +154,14 @@ export default function PartDetail() {
|
|||||||
name: 'sales_orders',
|
name: 'sales_orders',
|
||||||
label: t`Sales Orders`,
|
label: t`Sales Orders`,
|
||||||
icon: <IconTruckDelivery />,
|
icon: <IconTruckDelivery />,
|
||||||
hidden: !part.salable
|
hidden: !part.salable,
|
||||||
|
content: part.pk && (
|
||||||
|
<SalesOrderTable
|
||||||
|
params={{
|
||||||
|
part: part.pk ?? -1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'scheduling',
|
name: 'scheduling',
|
||||||
|
@ -73,6 +73,9 @@ export enum ApiPaths {
|
|||||||
build_order_list = 'api-build-list',
|
build_order_list = 'api-build-list',
|
||||||
build_order_attachment_list = 'api-build-attachment-list',
|
build_order_attachment_list = 'api-build-attachment-list',
|
||||||
|
|
||||||
|
// BOM URLs
|
||||||
|
bom_list = 'api-bom-list',
|
||||||
|
|
||||||
// Part URLs
|
// Part URLs
|
||||||
part_list = 'api-part-list',
|
part_list = 'api-part-list',
|
||||||
category_list = 'api-category-list',
|
category_list = 'api-category-list',
|
||||||
@ -159,6 +162,8 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'build/';
|
return 'build/';
|
||||||
case ApiPaths.build_order_attachment_list:
|
case ApiPaths.build_order_attachment_list:
|
||||||
return 'build/attachment/';
|
return 'build/attachment/';
|
||||||
|
case ApiPaths.bom_list:
|
||||||
|
return 'bom/';
|
||||||
case ApiPaths.part_list:
|
case ApiPaths.part_list:
|
||||||
return 'part/';
|
return 'part/';
|
||||||
case ApiPaths.part_parameter_list:
|
case ApiPaths.part_parameter_list:
|
||||||
|
Loading…
Reference in New Issue
Block a user