mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Build detail page (#5554)
* Add skeleton for build order detail page * Fill out some tabs * Add PlaceholderPanel component * Fix icon * Add child build order table * Add extra columns to build table * Add header and breadcrumbs to build detail pgae * Update part detail page * Improve BuildIndex page * Include part detail * Add consumed stock table * Add "completed outputs" table * PanelGroup tweaks * PartIndex tweaks
This commit is contained in:
parent
cd0dfab17b
commit
41cbe30db1
@ -1,6 +1,10 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Badge, Tooltip } from '@mantine/core';
|
||||
import { Alert, Badge, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* Small badge to indicate that a feature is a placeholder.
|
||||
*/
|
||||
export function PlaceholderPill() {
|
||||
return (
|
||||
<Tooltip
|
||||
@ -15,3 +19,20 @@ export function PlaceholderPill() {
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder panel for use in a PanelGroup.
|
||||
*/
|
||||
export function PlaceholderPanel() {
|
||||
return (
|
||||
<Stack>
|
||||
<Alert
|
||||
color="teal"
|
||||
title={t`This panel is a placeholder.`}
|
||||
icon={<IconInfoCircle />}
|
||||
>
|
||||
<Text color="gray">This panel has not yet been implemented</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
28
src/frontend/src/components/nav/BreadcrumbList.tsx
Normal file
28
src/frontend/src/components/nav/BreadcrumbList.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Anchor, Breadcrumbs, Paper, Text } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export type Breadcrumb = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a breadcrumb list, with integrated navigation.
|
||||
*/
|
||||
export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Paper p="3" radius="xs">
|
||||
<Breadcrumbs>
|
||||
{breadcrumbs.map((breadcrumb, index) => {
|
||||
return (
|
||||
<Anchor onClick={() => breadcrumb.url && navigate(breadcrumb.url)}>
|
||||
<Text size="sm">{breadcrumb.name}</Text>
|
||||
</Anchor>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
</Paper>
|
||||
);
|
||||
}
|
47
src/frontend/src/components/nav/PageDetail.tsx
Normal file
47
src/frontend/src/components/nav/PageDetail.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||
|
||||
/**
|
||||
* Construct a "standard" page detail for common display between pages.
|
||||
*
|
||||
* @param breadcrumbs - The breadcrumbs to display (optional)
|
||||
* @param
|
||||
*/
|
||||
export function PageDetail({
|
||||
title,
|
||||
subtitle,
|
||||
detail,
|
||||
breadcrumbs,
|
||||
actions
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
detail?: ReactNode;
|
||||
breadcrumbs?: Breadcrumb[];
|
||||
actions?: ReactNode[];
|
||||
}) {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<Paper p="xs" radius="xs" shadow="xs">
|
||||
<BreadcrumbList breadcrumbs={breadcrumbs} />
|
||||
</Paper>
|
||||
)}
|
||||
<Paper p="xs" radius="xs" shadow="xs">
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<Text size="xl">{title}</Text>
|
||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||
</Group>
|
||||
<Space />
|
||||
{actions && <Group position="right">{actions}</Group>}
|
||||
</Group>
|
||||
{detail}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -54,40 +54,41 @@ export function PanelGroup({
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activePanelName}
|
||||
orientation="vertical"
|
||||
onTabChange={handlePanelChange}
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Paper p="sm" radius="xs" shadow="xs">
|
||||
<Tabs
|
||||
value={activePanelName}
|
||||
orientation="vertical"
|
||||
onTabChange={handlePanelChange}
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List>
|
||||
{panels.map(
|
||||
(panel, idx) =>
|
||||
!panel.hidden && (
|
||||
<Tabs.Tab
|
||||
p="xs"
|
||||
value={panel.name}
|
||||
icon={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
>
|
||||
{panel.label}
|
||||
</Tabs.Tab>
|
||||
)
|
||||
)}
|
||||
</Tabs.List>
|
||||
{panels.map(
|
||||
(panel, idx) =>
|
||||
!panel.hidden && (
|
||||
<Tabs.Tab
|
||||
value={panel.name}
|
||||
icon={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
>
|
||||
{panel.label}
|
||||
</Tabs.Tab>
|
||||
)
|
||||
)}
|
||||
</Tabs.List>
|
||||
{panels.map(
|
||||
(panel, idx) =>
|
||||
!panel.hidden && (
|
||||
<Tabs.Panel key={idx} value={panel.name}>
|
||||
<Paper p="md" radius="xs">
|
||||
<Tabs.Panel key={idx} value={panel.name} p="sm">
|
||||
<Stack spacing="md">
|
||||
<Text size="xl">{panel.label}</Text>
|
||||
<Divider />
|
||||
{panel.content}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
)}
|
||||
</Tabs>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
)}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
@ -79,6 +79,8 @@ function saveActiveFilters(tableKey: string, filters: TableFilter[]) {
|
||||
|
||||
/**
|
||||
* Table Component which extends DataTable with custom InvenTree functionality
|
||||
*
|
||||
* TODO: Refactor table props into a single type
|
||||
*/
|
||||
export function InvenTreeTable({
|
||||
url,
|
||||
@ -99,6 +101,7 @@ export function InvenTreeTable({
|
||||
customActionGroups = [],
|
||||
customFilters = [],
|
||||
rowActions,
|
||||
onRowClick,
|
||||
refreshId
|
||||
}: {
|
||||
url: string;
|
||||
@ -119,6 +122,7 @@ export function InvenTreeTable({
|
||||
customActionGroups?: any[];
|
||||
customFilters?: TableFilter[];
|
||||
rowActions?: (record: any) => RowAction[];
|
||||
onRowClick?: (record: any, index: number, event: any) => void;
|
||||
refreshId?: string;
|
||||
}) {
|
||||
// Check if any columns are switchable (can be hidden)
|
||||
@ -507,6 +511,7 @@ export function InvenTreeTable({
|
||||
noRecordsText={missingRecordsText}
|
||||
records={data?.results ?? data ?? []}
|
||||
columns={dataColumns}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Progress } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||
import { TableColumn } from '../Column';
|
||||
@ -50,12 +51,6 @@ function buildOrderTableColumns(): TableColumn[] {
|
||||
// TODO: Hide this if project code is not enabled
|
||||
// TODO: Custom render function here
|
||||
},
|
||||
{
|
||||
accessor: 'priority',
|
||||
title: t`Priority`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'quantity',
|
||||
sortable: true,
|
||||
@ -87,16 +82,44 @@ function buildOrderTableColumns(): TableColumn[] {
|
||||
switchable: true
|
||||
// TODO: Custom render function here (status label)
|
||||
},
|
||||
{
|
||||
accessor: 'priority',
|
||||
title: t`Priority`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'creation_date',
|
||||
sortable: true,
|
||||
title: t`Created`,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'target_date',
|
||||
sortable: true,
|
||||
title: t`Target Date`,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'completion_date',
|
||||
sortable: true,
|
||||
title: t`Completed`,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'issued_by',
|
||||
sortable: true,
|
||||
title: t`Issued By`,
|
||||
switchable: true
|
||||
// TODO: custom render function
|
||||
},
|
||||
{
|
||||
accessor: 'responsible',
|
||||
sortable: true,
|
||||
title: t`Responsible`,
|
||||
switchable: true
|
||||
// TODO: custom render function
|
||||
}
|
||||
// TODO: issued_by
|
||||
// TODO: responsible
|
||||
// TODO: target_date
|
||||
// TODO: completion_date
|
||||
];
|
||||
}
|
||||
|
||||
@ -116,9 +139,11 @@ function buildOrderTableParams(params: any): any {
|
||||
*/
|
||||
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
||||
// Add required query parameters
|
||||
let tableParams = useMemo(() => buildOrderTableParams(params), [params]);
|
||||
let tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
||||
let tableFilters = useMemo(() => buildOrderTableFilters(), []);
|
||||
const tableParams = useMemo(() => buildOrderTableParams(params), [params]);
|
||||
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
||||
const tableFilters = useMemo(() => buildOrderTableFilters(), []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
tableParams.part_detail = true;
|
||||
|
||||
@ -130,6 +155,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
|
||||
params={tableParams}
|
||||
columns={tableColumns}
|
||||
customFilters={tableFilters}
|
||||
onRowClick={(row) => navigate(`/build/${row.pk}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -79,17 +79,6 @@ function stockItemTableColumns(): TableColumn[] {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a set of parameters for the stock item table
|
||||
*/
|
||||
function stockItemTableParams(params: any): any {
|
||||
return {
|
||||
...params,
|
||||
part_detail: true,
|
||||
location_detail: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list of available filters for the stock item table
|
||||
*/
|
||||
@ -113,7 +102,14 @@ function stockItemTableFilters(): TableFilter[] {
|
||||
* Load a table of stock items
|
||||
*/
|
||||
export function StockItemTable({ params = {} }: { params?: any }) {
|
||||
let tableParams = useMemo(() => stockItemTableParams(params), []);
|
||||
let tableParams = useMemo(() => {
|
||||
return {
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
...params
|
||||
};
|
||||
}, [params]);
|
||||
|
||||
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
||||
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Group } from '@mantine/core';
|
||||
|
||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
|
||||
|
||||
export default function Build() {
|
||||
return (
|
||||
<>
|
||||
<Group>
|
||||
<StylishText>
|
||||
<Trans>Build Orders</Trans>
|
||||
</StylishText>
|
||||
<PlaceholderPill />
|
||||
</Group>
|
||||
<BuildOrderTable />
|
||||
</>
|
||||
);
|
||||
}
|
166
src/frontend/src/pages/build/BuildDetail.tsx
Normal file
166
src/frontend/src/pages/build/BuildDetail.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
IconClipboardCheck,
|
||||
IconClipboardList,
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconListTree,
|
||||
IconNotes,
|
||||
IconPaperclip,
|
||||
IconSitemap
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import {
|
||||
PlaceholderPanel,
|
||||
PlaceholderPill
|
||||
} from '../../components/items/Placeholder';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
|
||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||
|
||||
/**
|
||||
* Detail page for a single Build Order
|
||||
*/
|
||||
export default function BuildDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
// Build data
|
||||
const [build, setBuild] = useState<any>({});
|
||||
|
||||
// Query hook for fetching build data
|
||||
const buildQuery = useQuery(['build', id ?? -1], async () => {
|
||||
let url = `/build/${id}/`;
|
||||
|
||||
return api
|
||||
.get(url, {
|
||||
params: {
|
||||
part_detail: true
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
setBuild(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setBuild({});
|
||||
});
|
||||
});
|
||||
|
||||
const buildPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'details',
|
||||
label: t`Build Details`,
|
||||
icon: <IconInfoCircle size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'allocate-stock',
|
||||
label: t`Allocate Stock`,
|
||||
icon: <IconListCheck size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
// TODO: Hide if build is complete
|
||||
},
|
||||
{
|
||||
name: 'incomplete-outputs',
|
||||
label: t`Incomplete Outputs`,
|
||||
icon: <IconClipboardList size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
// TODO: Hide if build is complete
|
||||
},
|
||||
{
|
||||
name: 'complete-outputs',
|
||||
label: t`Completed Outputs`,
|
||||
icon: <IconClipboardCheck size="18" />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
params={{
|
||||
build: build.pk ?? -1,
|
||||
is_building: false
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'consumed-stock',
|
||||
label: t`Consumed Stock`,
|
||||
icon: <IconList size="18" />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
params={{
|
||||
consumed_by: build.pk ?? -1
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'child-orders',
|
||||
label: t`Child Build Orders`,
|
||||
icon: <IconSitemap size="18" />,
|
||||
content: (
|
||||
<BuildOrderTable
|
||||
params={{
|
||||
parent: build.pk ?? -1
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip size="18" />,
|
||||
content: (
|
||||
<AttachmentTable
|
||||
url="/build/attachment/"
|
||||
model="build"
|
||||
pk={build.pk ?? -1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes size="18" />,
|
||||
content: (
|
||||
<NotesEditor
|
||||
url={`/build/${build.pk}/`}
|
||||
data={build.notes ?? ''}
|
||||
allowEdit={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [build]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<PageDetail
|
||||
title={t`Build Order`}
|
||||
subtitle={build.reference}
|
||||
detail={
|
||||
<Alert color="teal" title="Build order detail goes here">
|
||||
<Text>TODO: Build details</Text>
|
||||
</Alert>
|
||||
}
|
||||
breadcrumbs={[
|
||||
{ name: t`Build Orders`, url: '/build' },
|
||||
{ name: build.reference, url: `/build/${build.pk}` }
|
||||
]}
|
||||
actions={[<PlaceholderPill key="1" />]}
|
||||
/>
|
||||
<LoadingOverlay visible={buildQuery.isFetching} />
|
||||
<PanelGroup panels={buildPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
27
src/frontend/src/pages/build/BuildIndex.tsx
Normal file
27
src/frontend/src/pages/build/BuildIndex.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button, Stack, Text } from '@mantine/core';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
|
||||
import { notYetImplemented } from '../../functions/notifications';
|
||||
|
||||
/**
|
||||
* Build Order index page
|
||||
*/
|
||||
export default function BuildIndex() {
|
||||
return (
|
||||
<>
|
||||
<Stack>
|
||||
<PageDetail
|
||||
title={t`Build Orders`}
|
||||
actions={[
|
||||
<Button color="green" onClick={() => notYetImplemented()}>
|
||||
<Text>{t`New Build Order`}</Text>
|
||||
</Button>
|
||||
]}
|
||||
/>
|
||||
<BuildOrderTable />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
@ -29,6 +30,11 @@ import { useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import {
|
||||
PlaceholderPanel,
|
||||
PlaceholderPill
|
||||
} from '../../components/items/Placeholder';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
||||
@ -52,7 +58,7 @@ export default function PartDetail() {
|
||||
name: 'details',
|
||||
label: t`Details`,
|
||||
icon: <IconInfoCircle size="18" />,
|
||||
content: <Text>part details go here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
@ -65,61 +71,61 @@ export default function PartDetail() {
|
||||
label: t`Variants`,
|
||||
icon: <IconVersions size="18" />,
|
||||
hidden: !part.is_template,
|
||||
content: <Text>part variants go here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'bom',
|
||||
label: t`Bill of Materials`,
|
||||
icon: <IconListTree size="18" />,
|
||||
hidden: !part.assembly,
|
||||
content: part.assembly && <Text>part BOM goes here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'builds',
|
||||
label: t`Build Orders`,
|
||||
icon: <IconTools size="18" />,
|
||||
hidden: !part.assembly && !part.component,
|
||||
content: <Text>part builds go here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'used_in',
|
||||
label: t`Used In`,
|
||||
icon: <IconList size="18" />,
|
||||
hidden: !part.component,
|
||||
content: <Text>part used in goes here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'pricing',
|
||||
label: t`Pricing`,
|
||||
icon: <IconCurrencyDollar size="18" />,
|
||||
content: <Text>part pricing goes here</Text>
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'suppliers',
|
||||
label: t`Suppliers`,
|
||||
icon: <IconBuilding size="18" />,
|
||||
content: <Text>part suppliers go here</Text>,
|
||||
hidden: !part.purchaseable
|
||||
hidden: !part.purchaseable,
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'purchase_orders',
|
||||
label: t`Purchase Orders`,
|
||||
icon: <IconShoppingCart size="18" />,
|
||||
content: <Text>part purchase orders go here</Text>,
|
||||
content: <PlaceholderPanel />,
|
||||
hidden: !part.purchaseable
|
||||
},
|
||||
{
|
||||
name: 'sales_orders',
|
||||
label: t`Sales Orders`,
|
||||
icon: <IconTruckDelivery size="18" />,
|
||||
content: <Text>part sales orders go here</Text>,
|
||||
content: <PlaceholderPanel />,
|
||||
hidden: !part.salable
|
||||
},
|
||||
{
|
||||
name: 'test_templates',
|
||||
label: t`Test Templates`,
|
||||
icon: <IconTestPipe size="18" />,
|
||||
content: <Text>part test templates go here</Text>,
|
||||
content: <PlaceholderPanel />,
|
||||
hidden: !part.trackable
|
||||
},
|
||||
{
|
||||
@ -195,31 +201,38 @@ export default function PartDetail() {
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<PageDetail
|
||||
title={t`Part`}
|
||||
subtitle={part.full_name}
|
||||
detail={
|
||||
<Alert color="teal" title="Part detail goes here">
|
||||
<Text>TODO: Part details</Text>
|
||||
</Alert>
|
||||
}
|
||||
breadcrumbs={[
|
||||
{ name: t`Parts`, url: '/part' },
|
||||
{ name: '...', url: '' },
|
||||
{ name: part.full_name, url: `/part/${part.pk}` }
|
||||
]}
|
||||
actions={[
|
||||
<Button
|
||||
variant="outline"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
part.pk &&
|
||||
editPart({
|
||||
part_id: part.pk,
|
||||
callback: () => {
|
||||
partQuery.refetch();
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit Part
|
||||
</Button>
|
||||
]}
|
||||
/>
|
||||
<LoadingOverlay visible={partQuery.isFetching} />
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<Text size="lg">Part Detail</Text>
|
||||
<Text>{part.name}</Text>
|
||||
<Text size="sm">{part.description}</Text>
|
||||
</Group>
|
||||
<Space />
|
||||
<Text>In Stock: {part.total_in_stock}</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
part.pk &&
|
||||
editPart({
|
||||
part_id: part.pk,
|
||||
callback: () => {
|
||||
partQuery.refetch();
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit Part
|
||||
</Button>
|
||||
</Group>
|
||||
<PanelGroup panels={partPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -9,6 +9,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { PartListTable } from '../../components/tables/part/PartTable';
|
||||
|
||||
@ -41,10 +42,18 @@ export default function PartIndex() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<StylishText>
|
||||
<Trans>Parts</Trans>
|
||||
</StylishText>
|
||||
<Stack>
|
||||
<PageDetail
|
||||
title={t`Parts`}
|
||||
breadcrumbs={
|
||||
[
|
||||
// {
|
||||
// name: t`Parts`,
|
||||
// url: '/part',
|
||||
// }
|
||||
]
|
||||
}
|
||||
/>
|
||||
<PanelGroup panels={panels} />
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -12,8 +12,19 @@ export const Playground = Loadable(
|
||||
lazy(() => import('./pages/Index/Playground'))
|
||||
);
|
||||
export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex')));
|
||||
export const PartDetail = Loadable(
|
||||
lazy(() => import('./pages/part/PartDetail'))
|
||||
);
|
||||
|
||||
export const Stock = Loadable(lazy(() => import('./pages/Index/Stock')));
|
||||
export const Build = Loadable(lazy(() => import('./pages/Index/Build')));
|
||||
|
||||
export const BuildIndex = Loadable(
|
||||
lazy(() => import('./pages/build/BuildIndex'))
|
||||
);
|
||||
export const BuildDetail = Loadable(
|
||||
lazy(() => import('./pages/build/BuildDetail'))
|
||||
);
|
||||
|
||||
export const Scan = Loadable(lazy(() => import('./pages/Index/Scan')));
|
||||
|
||||
export const Dashboard = Loadable(
|
||||
@ -29,10 +40,6 @@ export const Profile = Loadable(
|
||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
||||
);
|
||||
|
||||
export const PartDetail = Loadable(
|
||||
lazy(() => import('./pages/part/PartDetail'))
|
||||
);
|
||||
|
||||
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||
export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
|
||||
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
|
||||
@ -92,7 +99,11 @@ export const router = createBrowserRouter(
|
||||
},
|
||||
{
|
||||
path: 'build/',
|
||||
element: <Build />
|
||||
element: <BuildIndex />
|
||||
},
|
||||
{
|
||||
path: 'build/:id',
|
||||
element: <BuildDetail />
|
||||
},
|
||||
{
|
||||
path: '/profile/:tabValue',
|
||||
|
Loading…
Reference in New Issue
Block a user