From 194780f0b7523e2c91013b3fe03fa345b4dafa66 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Jun 2026 10:25:43 +0000 Subject: [PATCH] AttributeGrid --- .../src/components/previews/AttributeGrid.tsx | 40 +++++ .../previews/models/PartPreview.tsx | 160 ++++++++++++++++++ .../src/states/PreviewDrawerState.tsx | 18 +- 3 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/components/previews/AttributeGrid.tsx create mode 100644 src/frontend/src/components/previews/models/PartPreview.tsx diff --git a/src/frontend/src/components/previews/AttributeGrid.tsx b/src/frontend/src/components/previews/AttributeGrid.tsx new file mode 100644 index 0000000000..8f0c6e2204 --- /dev/null +++ b/src/frontend/src/components/previews/AttributeGrid.tsx @@ -0,0 +1,40 @@ +import { Divider, SimpleGrid, Text, Title } from '@mantine/core'; +import type { ReactNode } from 'react'; + +export interface AttributeRow { + label: string; + value: ReactNode | null | undefined; +} + +export function AttributeGrid({ + title, + items +}: Readonly<{ + title: string; + items: AttributeRow[]; +}>) { + const valid = items.filter( + (item) => item.value !== null && item.value !== undefined + ); + + if (valid.length === 0) return null; + + return ( + <> + + {title} + + {valid.map((item) => ( + <> + + {item.label} + + + {item.value} + + + ))} + + + ); +} diff --git a/src/frontend/src/components/previews/models/PartPreview.tsx b/src/frontend/src/components/previews/models/PartPreview.tsx new file mode 100644 index 0000000000..d0e97e7bd6 --- /dev/null +++ b/src/frontend/src/components/previews/models/PartPreview.tsx @@ -0,0 +1,160 @@ +import { + ApiEndpoints, + ModelType, + TagsList, + apiUrl, + formatDecimal +} from '@lib/index'; +import { t } from '@lingui/core/macro'; +import { Group, Stack, Text } from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from '../../../contexts/ApiContext'; +import { useInstance } from '../../../hooks/UseInstance'; +import { ApiImage } from '../../images/ApiImage'; +import { AttributeGrid, type AttributeRow } from '../AttributeGrid'; +import type { PreviewType } from '../PreviewType'; + +function PartPreviewContent({ instance }: Readonly<{ instance: any }>) { + const api = useApi(); + + const { instance: requirements } = useInstance({ + endpoint: ApiEndpoints.part_requirements, + pk: instance?.pk, + hasPrimaryKey: true, + disabled: !instance?.pk, + defaultValue: {} + }); + + const { data: parameters = [] } = useQuery({ + queryKey: ['part-preview-parameters', instance?.pk], + enabled: !!instance?.pk, + queryFn: () => + api + .get(apiUrl(ApiEndpoints.parameter_list), { + params: { + model_type: ModelType.part, + model_id: instance.pk, + limit: 100 + } + }) + .then((res) => res.data?.results ?? []) + }); + + const totalStock = requirements?.total_stock ?? instance?.total_in_stock ?? 0; + const availableStock = + requirements?.unallocated_stock ?? instance?.unallocated_stock ?? 0; + const minStock = instance?.minimum_stock ?? 0; + const ordering = requirements?.ordering ?? 0; + const isPurchaseable = instance?.purchaseable ?? false; + + const category = instance?.category_detail; + + const stockItems: AttributeRow[] = !instance?.virtual + ? [ + { + label: t`In Stock`, + value: + formatDecimal(totalStock) + + (instance?.units ? ` ${instance.units}` : '') + }, + availableStock !== totalStock + ? { + label: t`Available`, + value: + formatDecimal(availableStock) + + (instance?.units ? ` ${instance.units}` : '') + } + : null, + minStock > 0 + ? { + label: t`Minimum`, + value: + formatDecimal(minStock) + + (instance?.units ? ` ${instance.units}` : '') + } + : null, + isPurchaseable && ordering > 0 + ? { + label: t`On Order`, + value: + formatDecimal(ordering) + + (instance?.units ? ` ${instance.units}` : '') + } + : null + ].filter(Boolean) + : []; + + const parameterItems: AttributeRow[] = parameters.map((param: any) => ({ + label: param.template_detail?.name, + value: + param.data + + (param.template_detail?.units ? ` ${param.template_detail.units}` : '') + })); + + return ( + + {/* Headline: image + name / description / IPN / category */} + + + + + {instance?.full_name || instance?.name} + + {instance?.description && ( + + {instance.description} + + )} + {instance?.IPN && ( + + + {t`IPN`}: + + + {instance.IPN} + + + )} + {category && ( + + + {t`Category`}: + + {category.pathstring ?? category.name} + + )} + + + + {instance?.tags?.length > 0 && } + + {!instance?.virtual && ( + + )} + + + ); +} + +export function PartPreviewComponent({ + instance, + modelId +}: Readonly<{ + instance: any; + modelId: number; +}>): PreviewType { + return { + title: instance?.full_name || instance?.name || `Part #${modelId}`, + preview: + }; +} diff --git a/src/frontend/src/states/PreviewDrawerState.tsx b/src/frontend/src/states/PreviewDrawerState.tsx index 9e2c82bb54..aa837f0492 100644 --- a/src/frontend/src/states/PreviewDrawerState.tsx +++ b/src/frontend/src/states/PreviewDrawerState.tsx @@ -1,15 +1,18 @@ import type { ModelType } from '@lib/enums/ModelType'; import { create } from 'zustand'; +import type { PreviewType } from '../components/previews/PreviewType'; interface PreviewDrawerStateProps { isOpen: boolean; modelType?: ModelType; id?: number | string; instance?: any; + preview?: PreviewType; openPreview: ( modelType: ModelType, id: number | string | undefined, - instance?: any + instance?: any, + preview?: PreviewType ) => void; closePreview: () => void; } @@ -20,13 +23,15 @@ export const usePreviewDrawerState = create()( modelType: undefined, id: undefined, instance: undefined, + preview: undefined, openPreview: ( modelType: ModelType, id: number | string | undefined, - instance?: any + instance?: any, + preview?: PreviewType ) => { - set({ modelType, id, instance, isOpen: true }); + set({ modelType, id, instance, preview, isOpen: true }); }, closePreview: () => { @@ -38,9 +43,12 @@ export const usePreviewDrawerState = create()( export function openGlobalPreview( modelType: ModelType, id?: number | string | undefined, - instance?: any + instance?: any, + preview?: PreviewType ) { - usePreviewDrawerState.getState().openPreview(modelType, id, instance); + usePreviewDrawerState + .getState() + .openPreview(modelType, id, instance, preview); } export function closeGlobalPreview() {