mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
[PUI] Stock location table (#5566)
* StockLocationTable component * Build out stocklocation page * Add extra columns * Skeleton for stockitem page * breadcrumbs * Fix attachment table
This commit is contained in:
parent
ad8df52b73
commit
981bfa344b
@ -1,7 +1,9 @@
|
|||||||
import { Divider, Paper, Stack, Tabs, Text } from '@mantine/core';
|
import { Divider, Paper, Stack, Tabs } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { StylishText } from '../items/StylishText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type used to specify a single panel in a panel group
|
* Type used to specify a single panel in a panel group
|
||||||
*/
|
*/
|
||||||
@ -81,7 +83,7 @@ export function PanelGroup({
|
|||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tabs.Panel key={idx} value={panel.name} p="sm">
|
<Tabs.Panel key={idx} value={panel.name} p="sm">
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
<Text size="xl">{panel.label}</Text>
|
<StylishText size="lg">{panel.label}</StylishText>
|
||||||
<Divider />
|
<Divider />
|
||||||
{panel.content}
|
{panel.content}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -225,6 +225,7 @@ export function AttachmentTable({
|
|||||||
tableKey={tableKey}
|
tableKey={tableKey}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
props={{
|
props={{
|
||||||
|
noRecordsText: t`No attachments found`,
|
||||||
enableSelection: true,
|
enableSelection: true,
|
||||||
customActionGroups: customActionGroups,
|
customActionGroups: customActionGroups,
|
||||||
rowActions: allowEdit && allowDelete ? rowActions : undefined,
|
rowActions: allowEdit && allowDelete ? rowActions : undefined,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { notYetImplemented } from '../../../functions/notifications';
|
import { notYetImplemented } from '../../../functions/notifications';
|
||||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
@ -120,6 +121,8 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="stock/"
|
url="stock/"
|
||||||
@ -130,6 +133,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
|||||||
enableSelection: true,
|
enableSelection: true,
|
||||||
customFilters: tableFilters,
|
customFilters: tableFilters,
|
||||||
rowActions: stockItemRowActions,
|
rowActions: stockItemRowActions,
|
||||||
|
onRowClick: (record) => navigate(`/stock/item/${record.pk}`),
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
|
import { TableColumn } from '../Column';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stock location table
|
||||||
|
*/
|
||||||
|
export function StockLocationTable({ params = {} }: { params?: any }) {
|
||||||
|
const { tableKey, refreshTable } = useTableRefresh('stocklocation');
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: 'name',
|
||||||
|
title: t`Name`,
|
||||||
|
switchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
title: t`Description`,
|
||||||
|
switchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'pathstring',
|
||||||
|
title: t`Path`,
|
||||||
|
sortable: true,
|
||||||
|
switchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'items',
|
||||||
|
title: t`Stock Items`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'structural',
|
||||||
|
title: t`Structural`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (record: any) => (record.structural ? 'Y' : 'N')
|
||||||
|
// TODO: custom 'true / false' label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'external',
|
||||||
|
title: t`External`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
render: (record: any) => (record.structural ? 'Y' : 'N')
|
||||||
|
// TODO: custom 'true / false' label,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url="stock/location/"
|
||||||
|
tableKey={tableKey}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
enableDownload: true,
|
||||||
|
params: params,
|
||||||
|
onRowClick: (record) => {
|
||||||
|
navigate(`/stock/location/${record.pk}`);
|
||||||
|
}
|
||||||
|
// TODO: allow for "tree view" with cascade
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Stack } from '@mantine/core';
|
|
||||||
import { IconPackages, IconSitemap } from '@tabler/icons-react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
|
||||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
|
||||||
|
|
||||||
export default function Stock() {
|
|
||||||
const categoryPanels: PanelType[] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'stock-items',
|
|
||||||
label: t`Stock Items`,
|
|
||||||
icon: <IconPackages size="18" />,
|
|
||||||
content: <StockItemTable />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sublocations',
|
|
||||||
label: t`Sublocations`,
|
|
||||||
icon: <IconSitemap size="18" />,
|
|
||||||
content: <PlaceholderPanel />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack>
|
|
||||||
<PageDetail title={t`Stock Items`} />
|
|
||||||
<PanelGroup panels={categoryPanels} />
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Stack, Text } from '@mantine/core';
|
import { LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCategory,
|
IconCategory,
|
||||||
IconListDetails,
|
IconListDetails,
|
||||||
@ -25,10 +25,11 @@ import { useInstance } from '../../hooks/UseInstance';
|
|||||||
export default function CategoryDetail({}: {}) {
|
export default function CategoryDetail({}: {}) {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const { instance: category, refreshInstance } = useInstance(
|
const {
|
||||||
'/part/category/',
|
instance: category,
|
||||||
id
|
refreshInstance,
|
||||||
);
|
instanceQuery
|
||||||
|
} = useInstance('/part/category/', id);
|
||||||
|
|
||||||
const categoryPanels: PanelType[] = useMemo(
|
const categoryPanels: PanelType[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -70,6 +71,7 @@ export default function CategoryDetail({}: {}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
<PageDetail
|
<PageDetail
|
||||||
title={t`Part Category`}
|
title={t`Part Category`}
|
||||||
detail={<Text>{category.name ?? 'Top level'}</Text>}
|
detail={<Text>{category.name ?? 'Top level'}</Text>}
|
||||||
|
@ -134,7 +134,13 @@ export default function PartDetail() {
|
|||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
label: t`Attachments`,
|
label: t`Attachments`,
|
||||||
icon: <IconPaperclip size="18" />,
|
icon: <IconPaperclip size="18" />,
|
||||||
content: partAttachmentsTab()
|
content: (
|
||||||
|
<AttachmentTable
|
||||||
|
url="/part/attachment/"
|
||||||
|
model="part"
|
||||||
|
pk={part.pk ?? -1}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
@ -145,16 +151,6 @@ export default function PartDetail() {
|
|||||||
];
|
];
|
||||||
}, [part]);
|
}, [part]);
|
||||||
|
|
||||||
function partAttachmentsTab(): React.ReactNode {
|
|
||||||
return (
|
|
||||||
<AttachmentTable
|
|
||||||
url="/part/attachment/"
|
|
||||||
model="part"
|
|
||||||
pk={part.pk ?? -1}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function partRelatedTab(): React.ReactNode {
|
function partRelatedTab(): React.ReactNode {
|
||||||
return <RelatedPartTable partId={part.pk ?? -1} />;
|
return <RelatedPartTable partId={part.pk ?? -1} />;
|
||||||
}
|
}
|
||||||
@ -181,6 +177,7 @@ export default function PartDetail() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
<PageDetail
|
<PageDetail
|
||||||
title={t`Part`}
|
title={t`Part`}
|
||||||
subtitle={part.full_name}
|
subtitle={part.full_name}
|
||||||
@ -210,7 +207,6 @@ export default function PartDetail() {
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
|
||||||
<PanelGroup panels={partPanels} />
|
<PanelGroup panels={partPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
75
src/frontend/src/pages/stock/LocationDetail.tsx
Normal file
75
src/frontend/src/pages/stock/LocationDetail.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||||
|
import { IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
|
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||||
|
import { StockLocationTable } from '../../components/tables/stock/StockLocationTable';
|
||||||
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
|
||||||
|
export default function Stock() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const {
|
||||||
|
instance: location,
|
||||||
|
refreshInstance,
|
||||||
|
instanceQuery
|
||||||
|
} = useInstance('/stock/location/', id);
|
||||||
|
|
||||||
|
const locationPanels: PanelType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'stock-items',
|
||||||
|
label: t`Stock Items`,
|
||||||
|
icon: <IconPackages size="18" />,
|
||||||
|
content: (
|
||||||
|
<StockItemTable
|
||||||
|
params={{
|
||||||
|
location: location.pk ?? null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sublocations',
|
||||||
|
label: t`Sublocations`,
|
||||||
|
icon: <IconSitemap size="18" />,
|
||||||
|
content: (
|
||||||
|
<StockLocationTable
|
||||||
|
params={{
|
||||||
|
parent: location.pk ?? null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [location, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack>
|
||||||
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
|
<PageDetail
|
||||||
|
title={t`Stock Items`}
|
||||||
|
detail={<Text>{location.name ?? 'Top level'}</Text>}
|
||||||
|
breadcrumbs={
|
||||||
|
location.pk
|
||||||
|
? [
|
||||||
|
{ name: t`Stock`, url: '/stock' },
|
||||||
|
{ name: '...', url: '' },
|
||||||
|
{
|
||||||
|
name: location.name ?? t`Top level`,
|
||||||
|
url: `/stock/location/${location.pk}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PanelGroup panels={locationPanels} />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
117
src/frontend/src/pages/stock/StockDetail.tsx
Normal file
117
src/frontend/src/pages/stock/StockDetail.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconBookmark,
|
||||||
|
IconBoxPadding,
|
||||||
|
IconHistory,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconNotes,
|
||||||
|
IconPaperclip,
|
||||||
|
IconSitemap,
|
||||||
|
IconTransferIn
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||||
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
|
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||||
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
|
||||||
|
export default function StockDetail() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const {
|
||||||
|
instance: stockitem,
|
||||||
|
refreshInstance,
|
||||||
|
instanceQuery
|
||||||
|
} = useInstance('/stock/', id, {
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const stockPanels: PanelType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: t`Details`,
|
||||||
|
icon: <IconInfoCircle size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tracking',
|
||||||
|
label: t`Stock Tracking`,
|
||||||
|
icon: <IconHistory size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'allocations',
|
||||||
|
label: t`Allocations`,
|
||||||
|
icon: <IconBookmark size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installed_items',
|
||||||
|
label: t`Installed Items`,
|
||||||
|
icon: <IconBoxPadding size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'child_items',
|
||||||
|
label: t`Child Items`,
|
||||||
|
icon: <IconSitemap size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachments',
|
||||||
|
label: t`Attachments`,
|
||||||
|
icon: <IconPaperclip size="18" />,
|
||||||
|
content: (
|
||||||
|
<AttachmentTable
|
||||||
|
url="/stock/attachment/"
|
||||||
|
model="stock_item"
|
||||||
|
pk={stockitem.pk ?? -1}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'notes',
|
||||||
|
label: t`Notes`,
|
||||||
|
icon: <IconNotes size="18" />,
|
||||||
|
content: (
|
||||||
|
<NotesEditor
|
||||||
|
url={`/stock/${stockitem.pk}/`}
|
||||||
|
data={stockitem.notes ?? ''}
|
||||||
|
allowEdit={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [stockitem, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
|
<PageDetail
|
||||||
|
title={t`Stock Items`}
|
||||||
|
subtitle={stockitem.part_detail?.full_name ?? 'name goes here'}
|
||||||
|
detail={
|
||||||
|
<Alert color="teal" title="Stock Item">
|
||||||
|
<Text>Quantity: {stockitem.quantity ?? 'idk'}</Text>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ name: t`Stock`, url: '/stock' },
|
||||||
|
{ name: '...', url: '' },
|
||||||
|
{
|
||||||
|
name: stockitem.part_detail?.full_name ?? 'name goes here',
|
||||||
|
url: `/stock/item/${stockitem.pk}`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<PanelGroup panels={stockPanels} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -19,7 +19,13 @@ export const PartDetail = Loadable(
|
|||||||
lazy(() => import('./pages/part/PartDetail'))
|
lazy(() => import('./pages/part/PartDetail'))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Stock = Loadable(lazy(() => import('./pages/Index/Stock')));
|
export const LocationDetail = Loadable(
|
||||||
|
lazy(() => import('./pages/stock/LocationDetail'))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StockDetail = Loadable(
|
||||||
|
lazy(() => import('./pages/stock/StockDetail'))
|
||||||
|
);
|
||||||
|
|
||||||
export const BuildIndex = Loadable(
|
export const BuildIndex = Loadable(
|
||||||
lazy(() => import('./pages/build/BuildIndex'))
|
lazy(() => import('./pages/build/BuildIndex'))
|
||||||
@ -102,7 +108,15 @@ export const router = createBrowserRouter(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stock/',
|
path: 'stock/',
|
||||||
element: <Stock />
|
element: <LocationDetail />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stock/location/:id',
|
||||||
|
element: <LocationDetail />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stock/item/:id',
|
||||||
|
element: <StockDetail />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'build/',
|
path: 'build/',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user