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() {