2
0
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:
Oliver
2024-03-01 01:23:05 +00:00
parent a652ed85aa
commit 64109a7974
5 changed files with 151 additions and 39 deletions

View File

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

View File

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

View File

@ -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}` }

View File

@ -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
}, },

View File

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