mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-19 13:35:40 +00:00
Implement details page for build order
This commit is contained in:
@ -68,7 +68,7 @@ export const StatusRenderer = ({
|
|||||||
type,
|
type,
|
||||||
options
|
options
|
||||||
}: {
|
}: {
|
||||||
status: string;
|
status: string | number;
|
||||||
type: ModelType | string;
|
type: ModelType | string;
|
||||||
options?: renderStatusLabelOptionsInterface;
|
options?: renderStatusLabelOptionsInterface;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -12,10 +12,12 @@ import {
|
|||||||
IconCopy,
|
IconCopy,
|
||||||
IconCornerUpRightDouble,
|
IconCornerUpRightDouble,
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
|
IconDotsCircleHorizontal,
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconFileUpload,
|
IconFileUpload,
|
||||||
IconGitBranch,
|
IconGitBranch,
|
||||||
IconGridDots,
|
IconGridDots,
|
||||||
|
IconHash,
|
||||||
IconLayersLinked,
|
IconLayersLinked,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconList,
|
IconList,
|
||||||
@ -23,14 +25,15 @@ import {
|
|||||||
IconMapPin,
|
IconMapPin,
|
||||||
IconMapPinHeart,
|
IconMapPinHeart,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
|
IconNumbers,
|
||||||
IconPackage,
|
IconPackage,
|
||||||
IconPackageImport,
|
IconPackageImport,
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
|
IconProgressCheck,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconRulerMeasure,
|
IconRulerMeasure,
|
||||||
IconShape,
|
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconShoppingCartHeart,
|
IconShoppingCartHeart,
|
||||||
IconStack2,
|
IconStack2,
|
||||||
@ -72,6 +75,8 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
revision: IconGitBranch,
|
revision: IconGitBranch,
|
||||||
units: IconRulerMeasure,
|
units: IconRulerMeasure,
|
||||||
keywords: IconTag,
|
keywords: IconTag,
|
||||||
|
status: IconInfoCircle,
|
||||||
|
info: IconInfoCircle,
|
||||||
details: IconInfoCircle,
|
details: IconInfoCircle,
|
||||||
parameters: IconList,
|
parameters: IconList,
|
||||||
stock: IconPackages,
|
stock: IconPackages,
|
||||||
@ -120,7 +125,10 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
user: IconUser,
|
user: IconUser,
|
||||||
group: IconUsersGroup,
|
group: IconUsersGroup,
|
||||||
check: IconCheck,
|
check: IconCheck,
|
||||||
copy: IconCopy
|
copy: IconCopy,
|
||||||
|
quantity: IconNumbers,
|
||||||
|
progress: IconProgressCheck,
|
||||||
|
reference: IconHash
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Group, LoadingOverlay, Skeleton, Stack, Table } from '@mantine/core';
|
import {
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
LoadingOverlay,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table
|
||||||
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconClipboardCheck,
|
IconClipboardCheck,
|
||||||
IconClipboardList,
|
IconClipboardList,
|
||||||
@ -17,6 +24,8 @@ import {
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DuplicateItemAction,
|
DuplicateItemAction,
|
||||||
@ -33,10 +42,12 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
|||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { buildOrderFields } from '../../forms/BuildForms';
|
import { buildOrderFields } from '../../forms/BuildForms';
|
||||||
|
import { partCategoryFields } from '../../forms/PartForms';
|
||||||
import { useEditApiFormModal } from '../../hooks/UseForm';
|
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import { DetailsField, DetailsTable } from '../../tables/Details';
|
||||||
import BuildLineTable from '../../tables/build/BuildLineTable';
|
import BuildLineTable from '../../tables/build/BuildLineTable';
|
||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||||
@ -63,36 +74,102 @@ export default function BuildDetail() {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildDetailsPanel = useMemo(() => {
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'part',
|
||||||
|
label: t`Part`,
|
||||||
|
model: ModelType.part
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'status',
|
||||||
|
name: 'status',
|
||||||
|
label: t`Status`,
|
||||||
|
model: ModelType.build
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
label: t`Reference`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'title',
|
||||||
|
label: t`Description`,
|
||||||
|
icon: 'description'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'quantity',
|
||||||
|
label: t`Build Quantity`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'completed',
|
||||||
|
icon: 'progress',
|
||||||
|
total: build.quantity,
|
||||||
|
progress: build.completed,
|
||||||
|
label: t`Completed Outputs`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'sales_order',
|
||||||
|
label: t`Sales Order`,
|
||||||
|
icon: 'sales_orders',
|
||||||
|
model: ModelType.salesorder,
|
||||||
|
model_field: 'reference',
|
||||||
|
hidden: !build.sales_order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'issued_by',
|
||||||
|
label: t`Issued By`,
|
||||||
|
badge: 'user',
|
||||||
|
icon: 'user'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'issued_by',
|
||||||
|
label: t`Issued By`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="apart" grow>
|
<ItemDetailsGrid>
|
||||||
<Table striped>
|
<Grid>
|
||||||
<tbody>
|
<Grid.Col span={4}>
|
||||||
<tr>
|
<DetailsImage
|
||||||
<td>{t`Base Part`}</td>
|
appRole={UserRoles.part}
|
||||||
<td>{build.part_detail?.name}</td>
|
apiPath={ApiEndpoints.part_list}
|
||||||
</tr>
|
src={build.part_detail?.thumbnail}
|
||||||
<tr>
|
pk={build.part}
|
||||||
<td>{t`Quantity`}</td>
|
|
||||||
<td>{build.quantity}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t`Build Status`}</td>
|
|
||||||
<td>
|
|
||||||
{build?.status && (
|
|
||||||
<StatusRenderer
|
|
||||||
status={build.status}
|
|
||||||
type={ModelType.build}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Grid.Col>
|
||||||
</td>
|
<Grid.Col span={8}>
|
||||||
</tr>
|
<DetailsTable fields={tl} item={build} />
|
||||||
</tbody>
|
</Grid.Col>
|
||||||
</Table>
|
</Grid>
|
||||||
<Table></Table>
|
<DetailsTable fields={tr} item={build} />
|
||||||
</Group>
|
<DetailsTable fields={bl} item={build} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
);
|
);
|
||||||
}, [build]);
|
}, [build, instanceQuery]);
|
||||||
|
|
||||||
const buildPanels: PanelType[] = useMemo(() => {
|
const buildPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -100,7 +177,7 @@ export default function BuildDetail() {
|
|||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Build Details`,
|
label: t`Build Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: buildDetailsPanel
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allocate-stock',
|
name: 'allocate-stock',
|
||||||
@ -259,7 +336,7 @@ export default function BuildDetail() {
|
|||||||
title={build.reference}
|
title={build.reference}
|
||||||
subtitle={build.title}
|
subtitle={build.title}
|
||||||
detail={buildDetail}
|
detail={buildDetail}
|
||||||
imageUrl={build.part_detail?.thumbnail}
|
imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ name: t`Build Orders`, url: '/build' },
|
{ name: t`Build Orders`, url: '/build' },
|
||||||
{ name: build.reference, url: `/build/${build.pk}` }
|
{ name: build.reference, url: `/build/${build.pk}` }
|
||||||
|
@ -103,7 +103,8 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
label: `Completed Tests`
|
label: `Completed Tests`,
|
||||||
|
icon: 'progress'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -136,7 +137,7 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'available_stock',
|
name: 'available_stock',
|
||||||
label: t`In Stock`
|
label: t`Available`
|
||||||
}
|
}
|
||||||
// TODO: allocated_to_sales_orders
|
// TODO: allocated_to_sales_orders
|
||||||
// TODO: allocated_to_build_orders
|
// TODO: allocated_to_build_orders
|
||||||
@ -220,7 +221,7 @@ export default function StockDetail() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Stock Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@ import { Suspense, useMemo } from 'react';
|
|||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { ProgressBar } from '../components/items/ProgressBar';
|
import { ProgressBar } from '../components/items/ProgressBar';
|
||||||
import { getModelInfo } from '../components/render/ModelType';
|
import { getModelInfo } from '../components/render/ModelType';
|
||||||
|
import { StatusRenderer } from '../components/render/StatusRenderer';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { InvenTreeIcon } from '../functions/icons';
|
import { InvenTreeIcon } from '../functions/icons';
|
||||||
@ -44,7 +45,7 @@ export type DetailsField =
|
|||||||
badge?: BadgeType;
|
badge?: BadgeType;
|
||||||
copy?: boolean;
|
copy?: boolean;
|
||||||
value_formatter?: () => ValueFormatterReturn;
|
value_formatter?: () => ValueFormatterReturn;
|
||||||
} & (StringDetailField | LinkDetailField | ProgressBarfield);
|
} & (StringDetailField | LinkDetailField | ProgressBarfield | StatusField);
|
||||||
|
|
||||||
type BadgeType = 'owner' | 'user' | 'group';
|
type BadgeType = 'owner' | 'user' | 'group';
|
||||||
type ValueFormatterReturn = string | number | null;
|
type ValueFormatterReturn = string | number | null;
|
||||||
@ -60,6 +61,8 @@ type LinkDetailField = {
|
|||||||
|
|
||||||
type InternalLinkField = {
|
type InternalLinkField = {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
|
model_field?: string;
|
||||||
|
model_formatter?: (value: any) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExternalLinkField = {
|
type ExternalLinkField = {
|
||||||
@ -72,6 +75,11 @@ type ProgressBarfield = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StatusField = {
|
||||||
|
type: 'status';
|
||||||
|
model: ModelType;
|
||||||
|
};
|
||||||
|
|
||||||
type FieldValueType = string | number | undefined;
|
type FieldValueType = string | number | undefined;
|
||||||
|
|
||||||
type FieldProps = {
|
type FieldProps = {
|
||||||
@ -327,6 +335,16 @@ function TableAnchorValue(props: FieldProps) {
|
|||||||
return getDetailUrl(props.field_data.model, props.field_value);
|
return getDetailUrl(props.field_data.model, props.field_value);
|
||||||
}, [props.field_data.model, props.field_value]);
|
}, [props.field_data.model, props.field_value]);
|
||||||
|
|
||||||
|
// Construct the "return value" for the fetched data
|
||||||
|
// Basic fallback value
|
||||||
|
let value = data?.name ?? 'No name defined';
|
||||||
|
|
||||||
|
if (props.field_data.model_formatter) {
|
||||||
|
value = props.field_data.model_formatter(data) ?? value;
|
||||||
|
} else if (props.field_data.model_field) {
|
||||||
|
value = data?.[props.field_data.model_field] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
||||||
<Anchor
|
<Anchor
|
||||||
@ -334,7 +352,7 @@ function TableAnchorValue(props: FieldProps) {
|
|||||||
target={data?.external ? '_blank' : undefined}
|
target={data?.external ? '_blank' : undefined}
|
||||||
rel={data?.external ? 'noreferrer noopener' : undefined}
|
rel={data?.external ? 'noreferrer noopener' : undefined}
|
||||||
>
|
>
|
||||||
<Text>{data.name ?? 'No name defined'}</Text>
|
<Text>{value}</Text>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
@ -350,6 +368,12 @@ function ProgressBarValue(props: FieldProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusValue(props: FieldProps) {
|
||||||
|
return (
|
||||||
|
<StatusRenderer type={props.field_data.model} status={props.field_value} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CopyField({ value }: { value: string }) {
|
function CopyField({ value }: { value: string }) {
|
||||||
return (
|
return (
|
||||||
<CopyButton value={value}>
|
<CopyButton value={value}>
|
||||||
@ -384,6 +408,8 @@ export function DetailsTableField({
|
|||||||
return TableAnchorValue;
|
return TableAnchorValue;
|
||||||
case 'progressbar':
|
case 'progressbar':
|
||||||
return ProgressBarValue;
|
return ProgressBarValue;
|
||||||
|
case 'status':
|
||||||
|
return StatusValue;
|
||||||
default:
|
default:
|
||||||
return TableStringValue;
|
return TableStringValue;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user