diff --git a/src/frontend/src/components/details/ItemDetails.tsx b/src/frontend/src/components/details/ItemDetails.tsx index d674cc7bcf..8252afc3a2 100644 --- a/src/frontend/src/components/details/ItemDetails.tsx +++ b/src/frontend/src/components/details/ItemDetails.tsx @@ -1,4 +1,5 @@ import { Grid, Group, Paper, SimpleGrid } from '@mantine/core'; +import React from 'react'; import { UserRoles } from '../../enums/Roles'; import { DetailsField, DetailsTable } from '../../tables/Details'; @@ -23,6 +24,16 @@ export type DetailsImageType = { imageActions: DetailImageButtonProps; }; +export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) { + return ( + + + {props.children} + + + ); +} + /** * Render a Details panel of the given model * @param params Object with the data of the model to render diff --git a/src/frontend/src/functions/urls.tsx b/src/frontend/src/functions/urls.tsx index 5920439c89..8125f7b02d 100644 --- a/src/frontend/src/functions/urls.tsx +++ b/src/frontend/src/functions/urls.tsx @@ -7,7 +7,7 @@ import { ModelType } from '../enums/ModelType'; export function getDetailUrl(model: ModelType, pk: number | string): string { const modelInfo = ModelInformationDict[model]; - if (modelInfo && modelInfo.url_detail) { + if (!!pk && modelInfo && modelInfo.url_detail) { return modelInfo.url_detail.replace(':pk', pk.toString()); } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 7d4ce146d9..9d448a1902 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -31,7 +31,8 @@ import { api } from '../../App'; import { DetailsImageType, ItemDetailFields, - ItemDetails + ItemDetails, + ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ActionDropdown, @@ -56,7 +57,7 @@ import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; -import { DetailsField } from '../../tables/Details'; +import { DetailsField, DetailsTable } from '../../tables/Details'; import { BomTable } from '../../tables/bom/BomTable'; import { UsedInTable } from '../../tables/bom/UsedInTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; @@ -441,6 +442,192 @@ export default function PartDetail() { return fields; }; + const detailsPanel = useMemo(() => { + if (instanceQuery.isFetching) { + return ; + } + + // Construct the details tables + let tl: DetailsField[] = [ + { + type: 'text', + name: 'description', + label: t`Description`, + copy: true + }, + { + type: 'link', + name: 'variant_of', + label: t`Variant of`, + model: ModelType.part, + hidden: !part.variant_of + } + ]; + + let tr: DetailsField[] = [ + { + type: 'string', + name: 'unallocated_stock', + unit: true, + label: t`Available Stock` + }, + { + type: 'string', + name: 'total_in_stock', + unit: true, + label: t`In Stock` + }, + { + type: 'string', + name: 'minimum_stock', + unit: true, + label: t`Minimum Stock`, + hidden: part.minimum_stock <= 0 + }, + { + type: 'string', + name: 'ordering', + label: t`On order`, + unit: true, + hidden: part.ordering <= 0 + }, + { + type: 'progressbar', + name: 'allocated_to_build_orders', + total: part.required_for_build_orders, + progress: part.allocated_to_build_orders, + label: t`Allocated to Build Orders`, + hidden: + !part.assembly || + (part.allocated_to_build_orders <= 0 && + part.required_for_build_orders <= 0) + }, + { + type: 'progressbar', + name: 'allocated_to_sales_orders', + total: part.required_for_sales_orders, + progress: part.allocated_to_sales_orders, + label: t`Allocated to Sales Orders`, + hidden: + !part.salable || + (part.allocated_to_sales_orders <= 0 && + part.required_for_sales_orders <= 0) + }, + { + type: 'string', + name: 'can_build', + unit: true, + label: t`Can Build`, + hidden: !part.assembly + }, + { + type: 'string', + name: 'building', + unit: true, + label: t`Building`, + hidden: !part.assembly + } + ]; + + let bl: DetailsField[] = [ + { + type: 'link', + name: 'category', + label: t`Category`, + model: ModelType.partcategory + }, + { + type: 'string', + name: 'IPN', + label: t`IPN`, + copy: true, + hidden: !part.IPN + }, + { + type: 'string', + name: 'revision', + label: t`Revision`, + copy: true, + hidden: !part.revision + }, + { + type: 'string', + name: 'units', + label: t`Units`, + hidden: !part.units + }, + { + type: 'string', + name: 'keywords', + label: t`Keywords`, + copy: true, + hidden: !part.keywords + }, + { + type: 'string', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !part.responsible + } + ]; + + let br: DetailsField[] = [ + { + type: 'string', + name: 'creation_date', + label: t`Creation Date` + }, + { + type: 'string', + name: 'creation_user', + badge: 'user' + }, + { + type: 'link', + name: 'default_location', + label: t`Default Location`, + model: ModelType.stocklocation, + hidden: !part.default_location + }, + { + type: 'link', + name: 'default_supplier', + label: t`Default Supplier`, + model: ModelType.supplierpart, + hidden: !part.default_supplier + }, + { + type: 'link', + name: 'link', + label: t`Link`, + external: true, + copy: true, + hidden: !part.link + } + ]; + + return ( + + + + + + + ); + + // content: !instanceQuery.isFetching && ( + // + // ) + }, [part, instanceQuery]); + // Part data panels (recalculate when part data changes) const partPanels: PanelType[] = useMemo(() => { return [ @@ -448,16 +635,7 @@ export default function PartDetail() { name: 'details', label: t`Details`, icon: , - content: !instanceQuery.isFetching && ( - - ) + content: detailsPanel }, { name: 'parameters', diff --git a/src/frontend/src/tables/Details.tsx b/src/frontend/src/tables/Details.tsx index b062635f5f..c4302f87ff 100644 --- a/src/frontend/src/tables/Details.tsx +++ b/src/frontend/src/tables/Details.tsx @@ -37,6 +37,7 @@ export type PartIconsType = { export type DetailsField = | { + hidden?: boolean; name: string; label?: string; badge?: BadgeType; @@ -299,7 +300,7 @@ function TableAnchorValue(props: FieldProps) { queryFn: async () => { const modelDef = getModelInfo(props.field_data.model); - if (!modelDef.api_endpoint) { + if (!modelDef?.api_endpoint) { return {}; } @@ -366,12 +367,12 @@ function CopyField({ value }: { value: string }) { ); } -function TableField({ +function xTableField({ field_data, field_value, unit = null }: { - field_data: DetailsField[]; + field_data: DetailsField; field_value: FieldValueType[]; unit?: string | null; }) { @@ -397,8 +398,8 @@ function TableField({ justifyContent: 'flex-start' }} > - - {field_data[0].label} + + {field_data.label}
@@ -428,52 +429,64 @@ function TableField({ ); } -export function DetailsTable({ +export function DetailsTableField({ item, - fields, - partIcons = false + field }: { item: any; - fields: DetailsField[][]; - partIcons?: boolean; + field: DetailsField; +}) { + function getFieldType(type: string) { + switch (type) { + case 'text': + case 'string': + return TableStringValue; + case 'link': + return TableAnchorValue; + case 'progressbar': + return ProgressBarValue; + default: + return TableStringValue; + } + } + + const FieldType: any = getFieldType(field.type); + + return ( + + + + {field.label} + + + + + {field.copy && } + + ); +} + +export function DetailsTable({ + item, + fields +}: { + item: any; + fields: DetailsField[]; }) { return ( - {partIcons && ( - - - - )} - {fields.map((data: DetailsField[], index: number) => { - let value: FieldValueType[] = []; - for (const val of data) { - if (val.value_formatter) { - value.push(undefined); - } else { - value.push(item[val.name]); - } - } - - return ( - - ); - })} + {fields.map((field: DetailsField, index: number) => ( + + ))}