diff --git a/src/frontend/src/components/previews/PreviewType.tsx b/src/frontend/src/components/previews/PreviewType.tsx
index bf6ab74fa2..8131348bf0 100644
--- a/src/frontend/src/components/previews/PreviewType.tsx
+++ b/src/frontend/src/components/previews/PreviewType.tsx
@@ -1,10 +1,12 @@
import { ModelType } from '@lib/enums/ModelType';
import type { ReactNode } from 'react';
+import { ManufacturerPartPreviewComponent } from './models/ManufacturerPartPreview';
import { PartPreviewComponent } from './models/PartPreview';
import { PurchaseOrderPreviewComponent } from './models/PurchaseOrderPreview';
import { ReturnOrderPreviewComponent } from './models/ReturnOrderPreview';
import { SalesOrderPreviewComponent } from './models/SalesOrderPreview';
import { StockPreviewComponent } from './models/StockPreview';
+import { SupplierPartPreviewComponent } from './models/SupplierPartPreview';
export interface PreviewType {
preview: ReactNode;
@@ -37,6 +39,10 @@ export function getPreviewComponentForModel({
return SalesOrderPreviewComponent({ instance, modelId });
case ModelType.returnorder:
return ReturnOrderPreviewComponent({ instance, modelId });
+ case ModelType.supplierpart:
+ return SupplierPartPreviewComponent({ instance, modelId });
+ case ModelType.manufacturerpart:
+ return ManufacturerPartPreviewComponent({ instance, modelId });
default:
return null;
}
diff --git a/src/frontend/src/components/previews/models/ManufacturerPartPreview.tsx b/src/frontend/src/components/previews/models/ManufacturerPartPreview.tsx
new file mode 100644
index 0000000000..df37a939a3
--- /dev/null
+++ b/src/frontend/src/components/previews/models/ManufacturerPartPreview.tsx
@@ -0,0 +1,26 @@
+import { t } from '@lingui/core/macro';
+import { ManufacturerPartDetailsPanel } from '../../../pages/company/ManufacturerPartDetailsPanel';
+import type { PreviewType } from '../PreviewType';
+
+export function ManufacturerPartPreviewComponent({
+ instance,
+ modelId
+}: Readonly<{
+ instance: any;
+ modelId: number;
+}>): PreviewType {
+ const manufacturer =
+ instance?.manufacturer_detail?.name ?? instance?.manufacturer_name;
+ const mpn = instance?.MPN ?? `#${modelId}`;
+
+ let title = `${t`Manufacturer Part`} ${mpn}`;
+
+ if (manufacturer) {
+ title += ` (${manufacturer})`;
+ }
+
+ return {
+ title,
+ preview:
+ };
+}
diff --git a/src/frontend/src/components/previews/models/SupplierPartPreview.tsx b/src/frontend/src/components/previews/models/SupplierPartPreview.tsx
new file mode 100644
index 0000000000..bb556c9ffe
--- /dev/null
+++ b/src/frontend/src/components/previews/models/SupplierPartPreview.tsx
@@ -0,0 +1,25 @@
+import { t } from '@lingui/core/macro';
+import { SupplierPartDetailsPanel } from '../../../pages/company/SupplierPartDetailsPanel';
+import type { PreviewType } from '../PreviewType';
+
+export function SupplierPartPreviewComponent({
+ instance,
+ modelId
+}: Readonly<{
+ instance: any;
+ modelId: number;
+}>): PreviewType {
+ const supplier = instance?.supplier_detail?.name ?? instance?.supplier_name;
+ const sku = instance?.SKU ?? `#${modelId}`;
+
+ let title = `${t`Supplier Part`} ${sku}`;
+
+ if (supplier) {
+ title += ` (${supplier})`;
+ }
+
+ return {
+ title,
+ preview:
+ };
+}
diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
index f9f1b5d57b..55529481e8 100644
--- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
+++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
-import { Grid, Skeleton, Stack } from '@mantine/core';
+import { Skeleton, Stack } from '@mantine/core';
import {
IconBuildingWarehouse,
IconInfoCircle,
@@ -8,20 +8,12 @@ import {
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import TagsList from '@lib/components/TagsList';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
-import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import type { PanelType } from '@lib/types/Panel';
import AdminButton from '../../components/buttons/AdminButton';
-import {
- type DetailsField,
- DetailsTable
-} from '../../components/details/Details';
-import { DetailsImage } from '../../components/details/DetailsImage';
-import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
DeleteItemAction,
DuplicateItemAction,
@@ -44,6 +36,7 @@ import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
+import { ManufacturerPartDetailsPanel } from './ManufacturerPartDetailsPanel';
export default function ManufacturerPartDetail() {
const { id } = useParams();
@@ -65,106 +58,19 @@ export default function ManufacturerPartDetail() {
}
});
- const detailsPanel = useMemo(() => {
- if (instanceQuery.isFetching) {
- return ;
- }
-
- const data = manufacturerPart ?? {};
-
- const tl: DetailsField[] = [
- {
- type: 'link',
- name: 'part',
- label: t`Internal Part`,
- model: ModelType.part,
- hidden: !manufacturerPart.part
- },
- {
- type: 'string',
- name: 'part_detail.IPN',
- label: t`IPN`,
- copy: true,
- icon: 'serial',
- hidden: !data.part_detail?.IPN
- },
- {
- type: 'string',
- name: 'part_detail.description',
- label: t`Description`,
- copy: true,
- icon: 'info',
- hidden: !manufacturerPart.description
- }
- ];
-
- const tr: DetailsField[] = [
- {
- type: 'link',
- name: 'manufacturer',
- label: t`Manufacturer`,
- icon: 'manufacturers',
- model: ModelType.company,
- hidden: !manufacturerPart.manufacturer
- },
- {
- type: 'string',
- name: 'MPN',
- label: t`Manufacturer Part Number`,
- copy: true,
- hidden: !manufacturerPart.MPN,
- icon: 'reference'
- },
- {
- type: 'string',
- name: 'description',
- label: t`Description`,
- copy: true,
- hidden: !manufacturerPart.description,
- icon: 'info'
- },
- {
- type: 'link',
- external: true,
- name: 'link',
- label: t`External Link`,
- copy: true,
- hidden: !manufacturerPart.link
- }
- ];
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
- }, [manufacturerPart, instanceQuery]);
-
const panels: PanelType[] = useMemo(() => {
return [
{
name: 'details',
label: t`Manufacturer Part Details`,
icon: ,
- content: detailsPanel
+ content: (
+
+ )
},
{
name: 'stock',
diff --git a/src/frontend/src/pages/company/ManufacturerPartDetailsPanel.tsx b/src/frontend/src/pages/company/ManufacturerPartDetailsPanel.tsx
new file mode 100644
index 0000000000..e8281266f7
--- /dev/null
+++ b/src/frontend/src/pages/company/ManufacturerPartDetailsPanel.tsx
@@ -0,0 +1,119 @@
+import { t } from '@lingui/core/macro';
+import { Grid, Skeleton, Stack } from '@mantine/core';
+
+import TagsList from '@lib/components/TagsList';
+import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
+import { ModelType } from '@lib/enums/ModelType';
+import { UserRoles } from '@lib/enums/Roles';
+import { apiUrl } from '@lib/functions/Api';
+
+import {
+ type DetailsField,
+ DetailsTable
+} from '../../components/details/Details';
+import { DetailsImage } from '../../components/details/DetailsImage';
+import { ItemDetailsGrid } from '../../components/details/ItemDetails';
+import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
+
+export function ManufacturerPartDetailsPanel({
+ instance,
+ allowImageEdit = false,
+ refreshInstance
+}: Readonly<{
+ instance: any;
+ allowImageEdit?: boolean;
+ refreshInstance?: () => void;
+}>) {
+ const tl: DetailsField[] = [
+ {
+ type: 'link',
+ name: 'part',
+ label: t`Internal Part`,
+ model: ModelType.part,
+ hidden: !instance?.part
+ },
+ {
+ type: 'string',
+ name: 'part_detail.IPN',
+ label: t`IPN`,
+ copy: true,
+ icon: 'serial',
+ hidden: !instance?.part_detail?.IPN
+ },
+ {
+ type: 'string',
+ name: 'part_detail.description',
+ label: t`Description`,
+ copy: true,
+ icon: 'info',
+ hidden: !instance?.description
+ }
+ ];
+
+ const tr: DetailsField[] = [
+ {
+ type: 'link',
+ name: 'manufacturer',
+ label: t`Manufacturer`,
+ icon: 'manufacturers',
+ model: ModelType.company,
+ hidden: !instance?.manufacturer
+ },
+ {
+ type: 'string',
+ name: 'MPN',
+ label: t`Manufacturer Part Number`,
+ copy: true,
+ hidden: !instance?.MPN,
+ icon: 'reference'
+ },
+ {
+ type: 'string',
+ name: 'description',
+ label: t`Description`,
+ copy: true,
+ hidden: !instance?.description,
+ icon: 'info'
+ },
+ {
+ type: 'link',
+ external: true,
+ name: 'link',
+ label: t`External Link`,
+ copy: true,
+ hidden: !instance?.link
+ }
+ ];
+
+ const parametersTable = useParameterDetailsGrid({
+ model_type: ModelType.manufacturerpart,
+ model_id: instance?.pk
+ });
+
+ if (!instance?.pk) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx
index e20d942491..fbdc880b74 100644
--- a/src/frontend/src/pages/company/SupplierPartDetail.tsx
+++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
-import { Grid, Skeleton, Stack } from '@mantine/core';
+import { Skeleton, Stack } from '@mantine/core';
import {
IconCurrencyDollar,
IconInfoCircle,
@@ -9,22 +9,14 @@ import {
import { type ReactNode, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import TagsList from '@lib/components/TagsList';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
-import { apiUrl } from '@lib/functions/Api';
import { formatDecimal } from '@lib/functions/Formatting';
import { getDetailUrl } from '@lib/functions/Navigation';
import type { PanelType } from '@lib/types/Panel';
import AdminButton from '../../components/buttons/AdminButton';
-import {
- type DetailsField,
- DetailsTable
-} from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
-import { DetailsImage } from '../../components/details/DetailsImage';
-import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
BarcodeActionDropdown,
DeleteItemAction,
@@ -49,6 +41,7 @@ import { useUserState } from '../../states/UserState';
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
import SupplierPriceBreakTable from '../../tables/purchasing/SupplierPriceBreakTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
+import { SupplierPartDetailsPanel } from './SupplierPartDetailsPanel';
export default function SupplierPartDetail() {
const { id } = useParams();
@@ -73,190 +66,19 @@ export default function SupplierPartDetail() {
}
});
- const detailsPanel = useMemo(() => {
- if (instanceQuery.isFetching) {
- return ;
- }
-
- const data = supplierPart ?? {};
-
- // Access nested data
- data.manufacturer =
- supplierPart.manufacturer || data.manufacturer_detail?.pk;
- data.MPN = supplierPart.MPN || data.manufacturer_part_detail?.MPN;
- data.manufacturer_part =
- supplierPart.manufacturer_part || data.manufacturer_part_detail?.pk;
-
- const tl: DetailsField[] = [
- {
- type: 'link',
- name: 'part',
- label: t`Internal Part`,
- model: ModelType.part,
- hidden: !supplierPart.part
- },
- {
- type: 'string',
- name: 'part_detail.IPN',
- label: t`IPN`,
- copy: true,
- hidden: !data.part_detail?.IPN,
- icon: 'serial'
- },
- {
- type: 'string',
- name: 'part_detail.description',
- label: t`Part Description`,
- copy: true,
- icon: 'info',
- hidden: !data.part_detail?.description
- },
- {
- type: 'link',
- external: true,
- name: 'link',
- label: t`External Link`,
- copy: true,
- hidden: !supplierPart.link
- },
- {
- type: 'string',
- name: 'note',
- label: t`Note`,
- copy: true,
- hidden: !supplierPart.note
- }
- ];
-
- const bl: DetailsField[] = [
- {
- type: 'link',
- name: 'supplier',
- label: t`Supplier`,
- model: ModelType.company,
- icon: 'suppliers',
- hidden: !supplierPart.supplier
- },
- {
- type: 'string',
- name: 'SKU',
- label: t`SKU`,
- copy: true,
- icon: 'reference'
- },
- {
- type: 'string',
- name: 'description',
- label: t`Description`,
- copy: true,
- hidden: !data.description
- },
- {
- type: 'link',
- name: 'manufacturer',
- label: t`Manufacturer`,
- model: ModelType.company,
- icon: 'manufacturers',
- hidden: !data.manufacturer
- },
- {
- type: 'link',
- name: 'manufacturer_part',
- model_field: 'MPN',
- label: t`Manufacturer Part`,
- model: ModelType.manufacturerpart,
- icon: 'reference',
- hidden: !data.manufacturer_part
- }
- ];
-
- const br: DetailsField[] = [
- {
- type: 'string',
- name: 'packaging',
- label: t`Packaging`,
- copy: true,
- hidden: !data.packaging
- },
- {
- type: 'string',
- name: 'pack_quantity',
- label: t`Pack Quantity`,
- copy: true,
- hidden: !data.pack_quantity,
- icon: 'packages'
- }
- ];
-
- const tr: DetailsField[] = [
- {
- type: 'number',
- name: 'in_stock',
- label: t`In Stock`,
- copy: true,
- icon: 'stock'
- },
- {
- type: 'number',
- name: 'on_order',
- label: t`On Order`,
- copy: true,
- icon: 'purchase_orders'
- },
- {
- type: 'number',
- name: 'available',
- label: t`Supplier Availability`,
- hidden: !data.availability_updated,
- copy: true,
- icon: 'packages'
- },
- {
- type: 'date',
- name: 'availability_updated',
- label: t`Availability Updated`,
- copy: true,
- hidden: !data.availability_updated,
- icon: 'calendar'
- }
- ];
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
- }, [supplierPart, instanceQuery.isFetching]);
-
const panels: PanelType[] = useMemo(() => {
return [
{
name: 'details',
label: t`Supplier Part Details`,
icon: ,
- content: detailsPanel
+ content: (
+
+ )
},
{
name: 'stock',
diff --git a/src/frontend/src/pages/company/SupplierPartDetailsPanel.tsx b/src/frontend/src/pages/company/SupplierPartDetailsPanel.tsx
new file mode 100644
index 0000000000..5f1d5a4458
--- /dev/null
+++ b/src/frontend/src/pages/company/SupplierPartDetailsPanel.tsx
@@ -0,0 +1,206 @@
+import { t } from '@lingui/core/macro';
+import { Grid, Skeleton, Stack } from '@mantine/core';
+import { useMemo } from 'react';
+
+import TagsList from '@lib/components/TagsList';
+import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
+import { ModelType } from '@lib/enums/ModelType';
+import { UserRoles } from '@lib/enums/Roles';
+import { apiUrl } from '@lib/functions/Api';
+
+import {
+ type DetailsField,
+ DetailsTable
+} from '../../components/details/Details';
+import { DetailsImage } from '../../components/details/DetailsImage';
+import { ItemDetailsGrid } from '../../components/details/ItemDetails';
+import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
+
+export function SupplierPartDetailsPanel({
+ instance,
+ allowImageEdit = false,
+ refreshInstance
+}: Readonly<{
+ instance: any;
+ allowImageEdit?: boolean;
+ refreshInstance?: () => void;
+}>) {
+ const data = useMemo(() => {
+ if (!instance) return {};
+ return {
+ ...instance,
+ manufacturer: instance.manufacturer || instance.manufacturer_detail?.pk,
+ MPN: instance.MPN || instance.manufacturer_part_detail?.MPN,
+ manufacturer_part:
+ instance.manufacturer_part || instance.manufacturer_part_detail?.pk
+ };
+ }, [instance]);
+
+ const tl: DetailsField[] = [
+ {
+ type: 'link',
+ name: 'part',
+ label: t`Internal Part`,
+ model: ModelType.part,
+ hidden: !instance?.part
+ },
+ {
+ type: 'string',
+ name: 'part_detail.IPN',
+ label: t`IPN`,
+ copy: true,
+ hidden: !data.part_detail?.IPN,
+ icon: 'serial'
+ },
+ {
+ type: 'string',
+ name: 'part_detail.description',
+ label: t`Part Description`,
+ copy: true,
+ icon: 'info',
+ hidden: !data.part_detail?.description
+ },
+ {
+ type: 'link',
+ external: true,
+ name: 'link',
+ label: t`External Link`,
+ copy: true,
+ hidden: !instance?.link
+ },
+ {
+ type: 'string',
+ name: 'note',
+ label: t`Note`,
+ copy: true,
+ hidden: !instance?.note
+ }
+ ];
+
+ const bl: DetailsField[] = [
+ {
+ type: 'link',
+ name: 'supplier',
+ label: t`Supplier`,
+ model: ModelType.company,
+ icon: 'suppliers',
+ hidden: !instance?.supplier
+ },
+ {
+ type: 'string',
+ name: 'SKU',
+ label: t`SKU`,
+ copy: true,
+ icon: 'reference'
+ },
+ {
+ type: 'string',
+ name: 'description',
+ label: t`Description`,
+ copy: true,
+ hidden: !data.description
+ },
+ {
+ type: 'link',
+ name: 'manufacturer',
+ label: t`Manufacturer`,
+ model: ModelType.company,
+ icon: 'manufacturers',
+ hidden: !data.manufacturer
+ },
+ {
+ type: 'link',
+ name: 'manufacturer_part',
+ model_field: 'MPN',
+ label: t`Manufacturer Part`,
+ model: ModelType.manufacturerpart,
+ icon: 'reference',
+ hidden: !data.manufacturer_part
+ }
+ ];
+
+ const br: DetailsField[] = [
+ {
+ type: 'string',
+ name: 'packaging',
+ label: t`Packaging`,
+ copy: true,
+ hidden: !data.packaging
+ },
+ {
+ type: 'string',
+ name: 'pack_quantity',
+ label: t`Pack Quantity`,
+ copy: true,
+ hidden: !data.pack_quantity,
+ icon: 'packages'
+ }
+ ];
+
+ const tr: DetailsField[] = [
+ {
+ type: 'number',
+ name: 'in_stock',
+ label: t`In Stock`,
+ copy: true,
+ icon: 'stock'
+ },
+ {
+ type: 'number',
+ name: 'on_order',
+ label: t`On Order`,
+ copy: true,
+ icon: 'purchase_orders'
+ },
+ {
+ type: 'number',
+ name: 'available',
+ label: t`Supplier Availability`,
+ hidden: !data.availability_updated,
+ copy: true,
+ icon: 'packages'
+ },
+ {
+ type: 'date',
+ name: 'availability_updated',
+ label: t`Availability Updated`,
+ copy: true,
+ hidden: !data.availability_updated,
+ icon: 'calendar'
+ }
+ ];
+
+ const parametersTable = useParameterDetailsGrid({
+ model_type: ModelType.supplierpart,
+ model_id: instance?.pk
+ });
+
+ if (!instance?.pk) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx
index b6b1981f4c..4c6fcdea89 100644
--- a/src/frontend/src/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/tables/InvenTreeTable.tsx
@@ -741,6 +741,8 @@ export function InvenTreeTableInternal>({
const accessor = tableProps.modelField ?? 'pk';
const pk = resolveItem(record, accessor);
+ console.log('row click:', tableProps.modelType, accessor, '->', pk);
+
if (pk) {
cancelEvent(event);
// If a model type is provided, navigate to the detail view for that model