AttributeGrid

This commit is contained in:
Oliver Walters 2026-06-30 10:25:43 +00:00
parent 448d500665
commit 194780f0b7
3 changed files with 213 additions and 5 deletions

View File

@ -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 (
<>
<Divider />
<Title order={4}>{title}</Title>
<SimpleGrid cols={2} spacing='xs'>
{valid.map((item) => (
<>
<Text key={`${item.label}-label`} fw={'bold'} size='sm'>
{item.label}
</Text>
<Text key={`${item.label}-value`} size='sm'>
{item.value}
</Text>
</>
))}
</SimpleGrid>
</>
);
}

View File

@ -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 (
<Stack gap='md'>
{/* Headline: image + name / description / IPN / category */}
<Group align='flex-start' gap='md'>
<ApiImage
src={
instance?.image ||
instance?.thumbnail ||
'/static/img/blank_image.png'
}
w={80}
h={80}
fit='contain'
radius='sm'
/>
<Stack gap={2} style={{ flex: 1 }}>
<Text fw={700} size='lg'>
{instance?.full_name || instance?.name}
</Text>
{instance?.description && (
<Text c='dimmed' size='sm'>
{instance.description}
</Text>
)}
{instance?.IPN && (
<Group gap='xs'>
<Text size='xs' c='dimmed'>
{t`IPN`}:
</Text>
<Text size='xs' fw={500}>
{instance.IPN}
</Text>
</Group>
)}
{category && (
<Group gap='xs'>
<Text size='xs' c='dimmed'>
{t`Category`}:
</Text>
<Text size='xs'>{category.pathstring ?? category.name}</Text>
</Group>
)}
</Stack>
</Group>
{instance?.tags?.length > 0 && <TagsList tags={instance.tags} />}
{!instance?.virtual && (
<AttributeGrid title={t`Stock Information`} items={stockItems} />
)}
<AttributeGrid title={t`Parameters`} items={parameterItems} />
</Stack>
);
}
export function PartPreviewComponent({
instance,
modelId
}: Readonly<{
instance: any;
modelId: number;
}>): PreviewType {
return {
title: instance?.full_name || instance?.name || `Part #${modelId}`,
preview: <PartPreviewContent instance={instance} />
};
}

View File

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