mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +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:
		@@ -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',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user