This commit is contained in:
Oliver Walters 2026-06-30 11:07:15 +00:00
parent b84bdac78f
commit ff7e4d4f3b
4 changed files with 139 additions and 73 deletions

View File

@ -7,7 +7,11 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import type { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl, navigateToLink } from '@lib/functions/Navigation';
import {
eventModified,
getDetailUrl,
navigateToLink
} from '@lib/functions/Navigation';
import type { TableFilter } from '@lib/types/Filters';
import { t } from '@lingui/core/macro';
import { ActionIcon, Group, Text } from '@mantine/core';
@ -171,7 +175,7 @@ export default function OrderCalendar({
const onClickOrder = (info: EventClickArg) => {
if (!info.event.id) return;
if (info.jsEvent.ctrlKey || info.jsEvent.metaKey || info.jsEvent.shiftKey) {
if (eventModified(event as any)) {
navigateToLink(
getDetailUrl(model, info.event.id),
navigate,

View File

@ -28,10 +28,10 @@ export function AttributeGrid({
<Table.Tbody>
{valid.map((item) => (
<Table.Tr key={item.label}>
<Table.Td w='50%' fw='bold'>
<Table.Td w='30%' fw='bold'>
{item.label}
</Table.Td>
<Table.Td w='50%'>{item.value}</Table.Td>
<Table.Td w='70%'>{item.value}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>

View File

@ -1,8 +1,20 @@
import { Anchor, Divider, Drawer, LoadingOverlay, Stack } from '@mantine/core';
import {
ActionIcon,
Anchor,
Divider,
Drawer,
Group,
LoadingOverlay,
Stack,
Tooltip
} from '@mantine/core';
import { StylishText } from '@lib/components/StylishText';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { eventModified } from '@lib/functions/Navigation';
import { type ModelType, getDetailUrl, navigateToLink } from '@lib/index';
import { t } from '@lingui/core/macro';
import { IconArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useInstance } from '../../hooks/UseInstance';
@ -73,7 +85,9 @@ export default function PreviewDrawer({
(event: MouseEvent) => {
if (!modelType || !id) return;
onClose();
if (!eventModified(event as any)) {
onClose();
}
navigateToLink(getDetailUrl(modelType!, id!), navigate, event);
},
[modelType, id, navigate]
@ -90,7 +104,14 @@ export default function PreviewDrawer({
href={getDetailUrl(modelType!, id, true)}
onClick={(e) => clickTitle(e)}
>
<StylishText size='lg'>{previewComponent.title}</StylishText>
<Group gap='xs'>
<Tooltip label={t`View Details`} position='left'>
<ActionIcon variant='transparent' size='lg'>
<IconArrowRight />
</ActionIcon>
</Tooltip>
<StylishText size='lg'>{previewComponent.title}</StylishText>
</Group>
</Anchor>
) : (
<StylishText size='lg'>{previewComponent.title}</StylishText>

View File

@ -8,11 +8,12 @@ import {
import { t } from '@lingui/core/macro';
import { Group, Stack, Text } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useApi } from '../../../contexts/ApiContext';
import { formatDate } from '../../../defaults/formatters';
import { formatDate, formatPriceRange } from '../../../defaults/formatters';
import { useInstance } from '../../../hooks/UseInstance';
import { ApiImage } from '../../images/ApiImage';
import { RenderPartCategory } from '../../render/Part';
import { RenderRemoteInstance } from '../../render/Instance';
import { RenderUser } from '../../render/User';
import { AttributeGrid, type AttributeRow } from '../AttributeGrid';
import type { PreviewType } from '../PreviewType';
@ -20,13 +21,11 @@ import type { PreviewType } from '../PreviewType';
function PartPreviewContent({ instance }: Readonly<{ instance: any }>) {
const api = useApi();
// Fetch part requirements
const { instance: requirements } = useInstance({
endpoint: ApiEndpoints.part_requirements,
pk: instance?.pk,
hasPrimaryKey: true,
disabled: !instance?.pk,
params: {},
defaultValue: {}
});
@ -45,49 +44,45 @@ function PartPreviewContent({ instance }: Readonly<{ instance: any }>) {
.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 units = instance?.units ? ` ${instance.units}` : '';
const category = instance?.category_detail;
const stockItems: AttributeRow[] = useMemo(() => {
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 canBuild = requirements?.can_build ?? 0;
const building = requirements?.building ?? 0;
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((x): x is NonNullable<typeof x> => x !== null)
: [];
return !instance?.virtual
? [
{
label: t`In Stock`,
value: formatDecimal(totalStock) + units
},
availableStock !== totalStock
? {
label: t`Available`,
value: formatDecimal(availableStock) + units
}
: null,
minStock > 0
? { label: t`Minimum`, value: formatDecimal(minStock) + units }
: null,
instance?.purchaseable && ordering > 0
? { label: t`On Order`, value: formatDecimal(ordering) + units }
: null,
instance?.assembly && canBuild >= 0
? { label: t`Can Build`, value: formatDecimal(canBuild) }
: null,
building > 0
? { label: t`In Production`, value: formatDecimal(building) }
: null
].filter((x): x is NonNullable<typeof x> => x !== null)
: [];
}, [requirements, instance, units]);
const parameterItems: AttributeRow[] = parameters.map((param: any) => ({
label: param.template_detail?.name,
@ -96,28 +91,64 @@ function PartPreviewContent({ instance }: Readonly<{ instance: any }>) {
(param.template_detail?.units ? ` ${param.template_detail.units}` : '')
}));
const infoItems: AttributeRow[] = [
{
label: t`Category`,
value: instance?.category_detail
? RenderPartCategory({ instance: instance.category_detail, link: true })
: null
},
{
label: t`Creation Date`,
value: instance?.creation_date ? formatDate(instance.creation_date) : null
},
{
label: t`Created By`,
value: instance?.creation_user_detail
? RenderUser({ instance: instance.creation_user_detail })
: null
}
];
const infoItems: AttributeRow[] = useMemo(() => {
const priceRange = formatPriceRange(
instance?.pricing_min,
instance?.pricing_max
);
return [
{
label: t`Category`,
value: instance?.category ? (
<RenderRemoteInstance
model={ModelType.partcategory}
pk={instance.category}
/>
) : null
},
{
label: t`Revision Of`,
value: instance?.revision_of ? (
<RenderRemoteInstance
model={ModelType.part}
pk={instance.revision_of}
/>
) : null
},
{
label: t`Variant Of`,
value: instance?.variant_of ? (
<RenderRemoteInstance
model={ModelType.part}
pk={instance.variant_of}
/>
) : null
},
{
label: t`Price Range`,
value:
instance?.pricing_min != null || instance?.pricing_max != null
? priceRange
: null
},
{
label: t`Creation Date`,
value: instance?.creation_date
? formatDate(instance.creation_date)
: null
},
{
label: t`Created By`,
value: instance?.creation_user_detail
? RenderUser({ instance: instance.creation_user_detail })
: null
}
];
}, [instance]);
return (
<Stack gap='md'>
{/* Headline: image + name / description / IPN / category */}
<Group align='flex-start' gap='md'>
<ApiImage
src={
@ -149,12 +180,22 @@ function PartPreviewContent({ instance }: Readonly<{ instance: any }>) {
</Text>
</Group>
)}
{category && (
{instance?.revision && (
<Group gap='xs'>
<Text size='xs' c='dimmed'>
{t`Revision`}:
</Text>
<Text size='xs' fw={500}>
{instance.revision}
</Text>
</Group>
)}
{instance?.category_name && (
<Group gap='xs'>
<Text size='xs' c='dimmed'>
{t`Category`}:
</Text>
<Text size='xs'>{category.pathstring ?? category.name}</Text>
<Text size='xs'>{instance?.category_name}</Text>
</Group>
)}
</Stack>