[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:
Oliver 2023-09-18 22:49:25 +10:00 committed by GitHub
parent ad8df52b73
commit 981bfa344b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 307 additions and 58 deletions

View File

@ -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 { useEffect, useState } from 'react';
import { StylishText } from '../items/StylishText';
/**
* Type used to specify a single panel in a panel group
*/
@ -81,7 +83,7 @@ export function PanelGroup({
!panel.hidden && (
<Tabs.Panel key={idx} value={panel.name} p="sm">
<Stack spacing="md">
<Text size="xl">{panel.label}</Text>
<StylishText size="lg">{panel.label}</StylishText>
<Divider />
{panel.content}
</Stack>

View File

@ -225,6 +225,7 @@ export function AttachmentTable({
tableKey={tableKey}
columns={tableColumns}
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
customActionGroups: customActionGroups,
rowActions: allowEdit && allowDelete ? rowActions : undefined,

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
@ -120,6 +121,8 @@ export function StockItemTable({ params = {} }: { params?: any }) {
return actions;
}
const navigate = useNavigate();
return (
<InvenTreeTable
url="stock/"
@ -130,6 +133,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
enableSelection: true,
customFilters: tableFilters,
rowActions: stockItemRowActions,
onRowClick: (record) => navigate(`/stock/item/${record.pk}`),
params: {
...params,
part_detail: true,

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Stack, Text } from '@mantine/core';
import { LoadingOverlay, Stack, Text } from '@mantine/core';
import {
IconCategory,
IconListDetails,
@ -25,10 +25,11 @@ import { useInstance } from '../../hooks/UseInstance';
export default function CategoryDetail({}: {}) {
const { id } = useParams();
const { instance: category, refreshInstance } = useInstance(
'/part/category/',
id
);
const {
instance: category,
refreshInstance,
instanceQuery
} = useInstance('/part/category/', id);
const categoryPanels: PanelType[] = useMemo(
() => [
@ -70,6 +71,7 @@ export default function CategoryDetail({}: {}) {
return (
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={t`Part Category`}
detail={<Text>{category.name ?? 'Top level'}</Text>}

View File

@ -134,7 +134,13 @@ export default function PartDetail() {
name: 'attachments',
label: t`Attachments`,
icon: <IconPaperclip size="18" />,
content: partAttachmentsTab()
content: (
<AttachmentTable
url="/part/attachment/"
model="part"
pk={part.pk ?? -1}
/>
)
},
{
name: 'notes',
@ -145,16 +151,6 @@ export default function PartDetail() {
];
}, [part]);
function partAttachmentsTab(): React.ReactNode {
return (
<AttachmentTable
url="/part/attachment/"
model="part"
pk={part.pk ?? -1}
/>
);
}
function partRelatedTab(): React.ReactNode {
return <RelatedPartTable partId={part.pk ?? -1} />;
}
@ -181,6 +177,7 @@ export default function PartDetail() {
return (
<>
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={t`Part`}
subtitle={part.full_name}
@ -210,7 +207,6 @@ export default function PartDetail() {
</Button>
]}
/>
<LoadingOverlay visible={instanceQuery.isFetching} />
<PanelGroup panels={partPanels} />
</Stack>
</>

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

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

View File

@ -19,7 +19,13 @@ export const PartDetail = Loadable(
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(
lazy(() => import('./pages/build/BuildIndex'))
@ -102,7 +108,15 @@ export const router = createBrowserRouter(
},
{
path: 'stock/',
element: <Stock />
element: <LocationDetail />
},
{
path: 'stock/location/:id',
element: <LocationDetail />
},
{
path: 'stock/item/:id',
element: <StockDetail />
},
{
path: 'build/',