location and category

This commit is contained in:
Oliver Walters 2026-06-30 14:34:43 +00:00
parent 763fab2033
commit 56ed757776
7 changed files with 279 additions and 198 deletions

View File

@ -3,11 +3,13 @@ import type { ReactNode } from 'react';
import { BuildOrderPreviewComponent } from './models/BuildOrderPreview';
import { CompanyPreviewComponent } from './models/CompanyPreview';
import { ManufacturerPartPreviewComponent } from './models/ManufacturerPartPreview';
import { PartCategoryPreviewComponent } from './models/PartCategoryPreview';
import { PartPreviewComponent } from './models/PartPreview';
import { PurchaseOrderPreviewComponent } from './models/PurchaseOrderPreview';
import { ReturnOrderPreviewComponent } from './models/ReturnOrderPreview';
import { SalesOrderPreviewComponent } from './models/SalesOrderPreview';
import { SalesOrderShipmentPreviewComponent } from './models/SalesOrderShipmentPreview';
import { StockLocationPreviewComponent } from './models/StockLocationPreview';
import { StockPreviewComponent } from './models/StockPreview';
import { SupplierPartPreviewComponent } from './models/SupplierPartPreview';
import { TransferOrderPreviewComponent } from './models/TransferOrderPreview';
@ -55,6 +57,10 @@ export function getPreviewComponentForModel({
return SalesOrderShipmentPreviewComponent({ instance, modelId });
case ModelType.transferorder:
return TransferOrderPreviewComponent({ instance, modelId });
case ModelType.stocklocation:
return StockLocationPreviewComponent({ instance, modelId });
case ModelType.partcategory:
return PartCategoryPreviewComponent({ instance, modelId });
default:
return null;
}

View File

@ -0,0 +1,18 @@
import { t } from '@lingui/core/macro';
import { PartCategoryDetailsPanel } from '../../../pages/part/PartCategoryDetailsPanel';
import type { PreviewType } from '../PreviewType';
export function PartCategoryPreviewComponent({
instance,
modelId
}: Readonly<{
instance: any;
modelId: number;
}>): PreviewType {
const name = `${t`Part Category`} - ${instance?.name ?? `#${modelId}`}`;
return {
title: name,
preview: <PartCategoryDetailsPanel instance={instance} />
};
}

View File

@ -0,0 +1,18 @@
import { t } from '@lingui/core/macro';
import { StockLocationDetailsPanel } from '../../../pages/stock/StockLocationDetailsPanel';
import type { PreviewType } from '../PreviewType';
export function StockLocationPreviewComponent({
instance,
modelId
}: Readonly<{
instance: any;
modelId: number;
}>): PreviewType {
const name = `${t`Stock Location`} - ${instance?.name ?? `#${modelId}`}`;
return {
title: name,
preview: <StockLocationDetailsPanel instance={instance} />
};
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import { Group, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import { LoadingOverlay, Stack } from '@mantine/core';
import {
IconCategory,
IconInfoCircle,
@ -21,8 +21,6 @@ import type { PanelType } from '@lib/types/Panel';
import { useLocalStorage } from '@mantine/hooks';
import AdminButton from '../../components/buttons/AdminButton';
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
DeleteItemAction,
EditItemAction,
@ -49,6 +47,7 @@ import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
import PartCategoryTemplateTable from '../../tables/part/PartCategoryTemplateTable';
import { PartListTable } from '../../tables/part/PartTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
import { PartCategoryDetailsPanel } from './PartCategoryDetailsPanel';
/**
* Detail view for a single PartCategory instance.
@ -103,106 +102,6 @@ export default function CategoryDetail() {
assign: false
});
const detailsPanel = useMemo(() => {
if (id && instanceQuery.isFetching) {
return <Skeleton />;
}
const left: DetailsField[] = [
{
type: 'text',
name: 'name',
label: t`Name`,
copy: true,
value_formatter: () => (
<Group gap='xs'>
{category.icon && <ApiIcon name={category.icon} />}
{category.name}
</Group>
)
},
{
type: 'text',
name: 'pathstring',
label: t`Path`,
icon: 'sitemap',
copy: true,
hidden: !id
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true
},
{
type: 'link',
name: 'parent',
model_field: 'name',
icon: 'location',
label: t`Parent Category`,
model: ModelType.partcategory,
hidden: !category?.parent
},
{
type: 'boolean',
name: 'starred',
icon: 'notification',
label: t`Subscribed`
}
];
const right: DetailsField[] = [
{
type: 'text',
name: 'part_count',
label: t`Parts`,
icon: 'part',
value_formatter: () => category?.part_count || '0'
},
{
type: 'text',
name: 'subcategories',
label: t`Subcategories`,
icon: 'sitemap',
hidden: !category?.subcategories
},
{
type: 'boolean',
name: 'structural',
label: t`Structural`,
icon: 'sitemap'
},
{
type: 'link',
name: 'parent_default_location',
label: t`Parent default location`,
model: ModelType.stocklocation,
hidden: !category.parent_default_location || category.default_location
},
{
type: 'link',
name: 'default_location',
label: t`Default location`,
model: ModelType.stocklocation,
hidden: !category.default_location
}
];
return (
<ItemDetailsGrid
tables={
id && category?.pk
? [
{ item: category, fields: left },
{ item: category, fields: right }
]
: []
}
/>
);
}, [category, instanceQuery]);
const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list,
pk: id,
@ -299,7 +198,7 @@ export default function CategoryDetail() {
name: 'details',
label: t`Category Details`,
icon: <IconInfoCircle />,
content: detailsPanel,
content: <PartCategoryDetailsPanel instance={category} />,
hidden: !id || !category?.pk
},
{

View File

@ -0,0 +1,121 @@
import { t } from '@lingui/core/macro';
import { Group, Skeleton } from '@mantine/core';
import { useMemo } from 'react';
import { ModelType } from '@lib/enums/ModelType';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
import { ApiIcon } from '../../components/items/ApiIcon';
export function PartCategoryDetailsPanel({
instance
}: Readonly<{
instance: any;
}>) {
const left: DetailsField[] = useMemo(
() => [
{
type: 'text',
name: 'name',
label: t`Name`,
copy: true,
value_formatter: () => (
<Group gap='xs'>
{instance?.icon && <ApiIcon name={instance.icon} />}
{instance?.name}
</Group>
)
},
{
type: 'text',
name: 'pathstring',
label: t`Path`,
icon: 'sitemap',
copy: true,
hidden: !instance?.pathstring
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true
},
{
type: 'link',
name: 'parent',
model_field: 'name',
icon: 'location',
label: t`Parent Category`,
model: ModelType.partcategory,
hidden: !instance?.parent
},
{
type: 'boolean',
name: 'starred',
icon: 'notification',
label: t`Subscribed`
}
],
[instance]
);
const right: DetailsField[] = useMemo(
() => [
{
type: 'text',
name: 'part_count',
label: t`Parts`,
icon: 'part',
value_formatter: () => instance?.part_count || '0'
},
{
type: 'text',
name: 'subcategories',
label: t`Subcategories`,
icon: 'sitemap',
hidden: !instance?.subcategories
},
{
type: 'boolean',
name: 'structural',
label: t`Structural`,
icon: 'sitemap'
},
{
type: 'link',
name: 'parent_default_location',
label: t`Parent default location`,
model: ModelType.stocklocation,
hidden:
!instance?.parent_default_location || !!instance?.default_location
},
{
type: 'link',
name: 'default_location',
label: t`Default location`,
model: ModelType.stocklocation,
hidden: !instance?.default_location
}
],
[instance]
);
const parametersTable = useParameterDetailsGrid({
model_type: ModelType.partcategory,
model_id: instance?.pk
});
if (!instance?.pk) return <Skeleton />;
return (
<ItemDetailsGrid
tables={[
{ item: instance, fields: left },
{ item: instance, fields: right },
parametersTable
]}
/>
);
}

View File

@ -7,7 +7,7 @@ import type { TableFilter } from '@lib/index';
import type { StockOperationProps } from '@lib/types/Forms';
import type { PanelType } from '@lib/types/Panel';
import { t } from '@lingui/core/macro';
import { Group, Skeleton, Stack } from '@mantine/core';
import { Skeleton, Stack } from '@mantine/core';
import {
IconCalendar,
IconInfoCircle,
@ -24,8 +24,6 @@ import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialo
import AdminButton from '../../components/buttons/AdminButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import OrderCalendar from '../../components/calendar/OrderCalendar';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
BarcodeActionDropdown,
DeleteItemAction,
@ -57,6 +55,7 @@ import StockLocationParametricTable from '../../tables/stock/StockLocationParame
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
import TransferOrderParametricTable from '../../tables/stock/TransferOrderParametricTable';
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
import { StockLocationDetailsPanel } from './StockLocationDetailsPanel';
function TransferOrderCalendar() {
const calendarFilters: TableFilter[] = useMemo(() => {
@ -102,98 +101,12 @@ export default function Stock() {
}
});
const detailsPanel = useMemo(() => {
if (id && instanceQuery.isFetching) {
return <Skeleton />;
}
const left: DetailsField[] = [
{
type: 'text',
name: 'name',
label: t`Name`,
copy: true,
value_formatter: () => (
<Group gap='xs'>
{location.icon && <ApiIcon name={location.icon} />}
{location.name}
</Group>
)
},
{
type: 'text',
name: 'pathstring',
label: t`Path`,
icon: 'sitemap',
copy: true,
hidden: !id
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true
},
{
type: 'link',
name: 'parent',
model_field: 'name',
icon: 'location',
label: t`Parent Location`,
model: ModelType.stocklocation,
hidden: !location?.parent
}
];
const right: DetailsField[] = [
{
type: 'text',
name: 'items',
icon: 'stock',
label: t`Stock Items`,
value_formatter: () => location?.items || '0'
},
{
type: 'text',
name: 'sublocations',
icon: 'location',
label: t`Sublocations`,
hidden: !location?.sublocations
},
{
type: 'boolean',
name: 'structural',
label: t`Structural`,
icon: 'sitemap'
},
{
type: 'boolean',
name: 'external',
label: t`External`
},
{
type: 'string',
// TODO: render location type icon here (ref: #7237)
name: 'location_type_detail.name',
label: t`Location Type`,
hidden: !location?.location_type,
icon: 'packages'
}
];
return (
<ItemDetailsGrid
tables={
id && location?.pk
? [
{ item: location, fields: left },
{ item: location, fields: right }
]
: []
}
/>
const detailsPanel =
id && instanceQuery.isFetching ? (
<Skeleton />
) : (
<StockLocationDetailsPanel instance={id ? location : undefined} />
);
}, [location, instanceQuery]);
const [sublocationView, setSublocationView] = useState<string>('table');
const [transferOrderView, setTransferOrderView] = useState<string>('table');

View File

@ -0,0 +1,106 @@
import { t } from '@lingui/core/macro';
import { Group, Skeleton } from '@mantine/core';
import { ModelType } from '@lib/enums/ModelType';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
import { ApiIcon } from '../../components/items/ApiIcon';
export function StockLocationDetailsPanel({
instance
}: Readonly<{
instance: any;
}>) {
const left: DetailsField[] = [
{
type: 'text',
name: 'name',
label: t`Name`,
copy: true,
value_formatter: () => (
<Group gap='xs'>
{instance?.icon && <ApiIcon name={instance.icon} />}
{instance?.name}
</Group>
)
},
{
type: 'text',
name: 'pathstring',
label: t`Path`,
icon: 'sitemap',
copy: true,
hidden: !instance?.pathstring
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true,
hidden: !instance?.description
},
{
type: 'link',
name: 'parent',
model_field: 'name',
icon: 'location',
label: t`Parent Location`,
model: ModelType.stocklocation,
hidden: !instance?.parent
}
];
const right: DetailsField[] = [
{
type: 'text',
name: 'items',
icon: 'stock',
label: t`Stock Items`,
value_formatter: () => instance?.items || '0'
},
{
type: 'text',
name: 'sublocations',
icon: 'location',
label: t`Sublocations`,
hidden: !instance?.sublocations
},
{
type: 'boolean',
name: 'structural',
label: t`Structural`,
icon: 'sitemap'
},
{
type: 'boolean',
name: 'external',
label: t`External`
},
{
type: 'string',
name: 'location_type_detail.name',
label: t`Location Type`,
hidden: !instance?.location_type,
icon: 'packages'
}
];
const parametersTable = useParameterDetailsGrid({
model_type: ModelType.stocklocation,
model_id: instance?.pk
});
if (!instance?.pk) return <Skeleton />;
return (
<ItemDetailsGrid
tables={[
{ item: instance, fields: left },
{ item: instance, fields: right },
parametersTable
]}
/>
);
}