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