From 56ed75777652ddeae76bf41d3f014ce437beb034 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Jun 2026 14:34:43 +0000 Subject: [PATCH] location and category --- .../src/components/previews/PreviewType.tsx | 6 + .../previews/models/PartCategoryPreview.tsx | 18 +++ .../previews/models/StockLocationPreview.tsx | 18 +++ .../src/pages/part/CategoryDetail.tsx | 107 +--------------- .../pages/part/PartCategoryDetailsPanel.tsx | 121 ++++++++++++++++++ .../src/pages/stock/LocationDetail.tsx | 101 +-------------- .../pages/stock/StockLocationDetailsPanel.tsx | 106 +++++++++++++++ 7 files changed, 279 insertions(+), 198 deletions(-) create mode 100644 src/frontend/src/components/previews/models/PartCategoryPreview.tsx create mode 100644 src/frontend/src/components/previews/models/StockLocationPreview.tsx create mode 100644 src/frontend/src/pages/part/PartCategoryDetailsPanel.tsx create mode 100644 src/frontend/src/pages/stock/StockLocationDetailsPanel.tsx diff --git a/src/frontend/src/components/previews/PreviewType.tsx b/src/frontend/src/components/previews/PreviewType.tsx index c3b0c3a433..5283f65389 100644 --- a/src/frontend/src/components/previews/PreviewType.tsx +++ b/src/frontend/src/components/previews/PreviewType.tsx @@ -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; } diff --git a/src/frontend/src/components/previews/models/PartCategoryPreview.tsx b/src/frontend/src/components/previews/models/PartCategoryPreview.tsx new file mode 100644 index 0000000000..537f6c97e4 --- /dev/null +++ b/src/frontend/src/components/previews/models/PartCategoryPreview.tsx @@ -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: + }; +} diff --git a/src/frontend/src/components/previews/models/StockLocationPreview.tsx b/src/frontend/src/components/previews/models/StockLocationPreview.tsx new file mode 100644 index 0000000000..ffe0dc6536 --- /dev/null +++ b/src/frontend/src/components/previews/models/StockLocationPreview.tsx @@ -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: + }; +} diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index b457719353..57c73f19be 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -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 ; - } - - const left: DetailsField[] = [ - { - type: 'text', - name: 'name', - label: t`Name`, - copy: true, - value_formatter: () => ( - - {category.icon && } - {category.name} - - ) - }, - { - 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 ( - - ); - }, [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: , - content: detailsPanel, + content: , hidden: !id || !category?.pk }, { diff --git a/src/frontend/src/pages/part/PartCategoryDetailsPanel.tsx b/src/frontend/src/pages/part/PartCategoryDetailsPanel.tsx new file mode 100644 index 0000000000..d8cb7f881a --- /dev/null +++ b/src/frontend/src/pages/part/PartCategoryDetailsPanel.tsx @@ -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: () => ( + + {instance?.icon && } + {instance?.name} + + ) + }, + { + 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 ; + + return ( + + ); +} diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 7572e588d5..bcaf1c08b3 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -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 ; - } - - const left: DetailsField[] = [ - { - type: 'text', - name: 'name', - label: t`Name`, - copy: true, - value_formatter: () => ( - - {location.icon && } - {location.name} - - ) - }, - { - 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 ( - + const detailsPanel = + id && instanceQuery.isFetching ? ( + + ) : ( + ); - }, [location, instanceQuery]); const [sublocationView, setSublocationView] = useState('table'); const [transferOrderView, setTransferOrderView] = useState('table'); diff --git a/src/frontend/src/pages/stock/StockLocationDetailsPanel.tsx b/src/frontend/src/pages/stock/StockLocationDetailsPanel.tsx new file mode 100644 index 0000000000..22fb631548 --- /dev/null +++ b/src/frontend/src/pages/stock/StockLocationDetailsPanel.tsx @@ -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: () => ( + + {instance?.icon && } + {instance?.name} + + ) + }, + { + 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 ; + + return ( + + ); +}