From 160d014e44ffa686bb0a6f2e96297a30856bb18e Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Mar 2024 17:12:53 +1100 Subject: [PATCH] [PUI] Details Pages (#6718) * Add "details" view to SupplierPart page * Fix PartActions * Add placeholder for actions * Add "title" option to DetailsTable * Add edit form to supplier part page * Fix link to manufacturer part * Add "details" view to ManufacturerPartDetail page * Add edit for ManufacturerPart * Create new manufacturer part from company table * Tweak ActionIcon --- .../src/components/buttons/ActionButton.tsx | 2 +- .../src/components/details/Details.tsx | 27 +- src/frontend/src/functions/icons.tsx | 3 + .../pages/company/ManufacturerPartDetail.tsx | 164 +++++++++++-- .../src/pages/company/SupplierPartDetail.tsx | 230 ++++++++++++++++-- src/frontend/src/pages/part/PartDetail.tsx | 9 +- .../purchasing/ManufacturerPartTable.tsx | 54 ++-- 7 files changed, 425 insertions(+), 64 deletions(-) diff --git a/src/frontend/src/components/buttons/ActionButton.tsx b/src/frontend/src/components/buttons/ActionButton.tsx index 2a948f39d6..caa5bf1363 100644 --- a/src/frontend/src/components/buttons/ActionButton.tsx +++ b/src/frontend/src/components/buttons/ActionButton.tsx @@ -39,7 +39,7 @@ export function ActionButton(props: ActionButtonProps) { color={props.color} size={props.size} onClick={props.onClick ?? notYetImplemented} - variant={props.variant} + variant={props.variant ?? 'light'} > {props.icon} diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 8996059747..201684d59e 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -7,6 +7,7 @@ import { Group, Paper, Skeleton, + Stack, Table, Text, Tooltip @@ -22,6 +23,7 @@ import { getDetailUrl } from '../../functions/urls'; import { apiUrl } from '../../states/ApiState'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { ProgressBar } from '../items/ProgressBar'; +import { StylishText } from '../items/StylishText'; import { YesNoButton } from '../items/YesNoButton'; import { getModelInfo } from '../render/ModelType'; import { StatusRenderer } from '../render/StatusRenderer'; @@ -385,22 +387,27 @@ export function DetailsTableField({ export function DetailsTable({ item, - fields + fields, + title }: { item: any; fields: DetailsField[]; + title?: string; }) { return ( - - - {fields - .filter((field: DetailsField) => !field.hidden) - .map((field: DetailsField, index: number) => ( - - ))} - -
+ + {title && {title}} + + + {fields + .filter((field: DetailsField) => !field.hidden) + .map((field: DetailsField, index: number) => ( + + ))} + +
+
); } diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 223baaf532..18a5886dd2 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -117,12 +117,15 @@ const icons = { test_templates: IconTestPipe, related_parts: IconLayersLinked, attachments: IconPaperclip, + note: IconNotes, notes: IconNotes, photo: IconPhoto, upload: IconFileUpload, reject: IconX, select_image: IconGridDots, delete: IconTrash, + packaging: IconPackage, + packages: IconPackages, // Part Icons active: IconCheck, diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx index 17f64d600c..8fad09054f 100644 --- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx +++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx @@ -1,7 +1,8 @@ import { t } from '@lingui/macro'; -import { LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { IconBuildingWarehouse, + IconDots, IconInfoCircle, IconList, IconPaperclip @@ -9,18 +10,38 @@ import { import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import { DetailsField, DetailsTable } from '../../components/details/Details'; +import { DetailsImage } from '../../components/details/DetailsImage'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; +import { + ActionDropdown, + DeleteItemAction, + DuplicateItemAction, + EditItemAction +} from '../../components/items/ActionDropdown'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useManufacturerPartFields } from '../../forms/CompanyForms'; +import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import ManufacturerPartParameterTable from '../../tables/purchasing/ManufacturerPartParameterTable'; import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; export default function ManufacturerPartDetail() { const { id } = useParams(); + const user = useUserState(); - const { instance: manufacturerPart, instanceQuery } = useInstance({ + const { + instance: manufacturerPart, + instanceQuery, + refreshInstance + } = useInstance({ endpoint: ApiEndpoints.manufacturer_part_list, pk: id, hasPrimaryKey: true, @@ -30,12 +51,91 @@ export default function ManufacturerPartDetail() { } }); + const detailsPanel = useMemo(() => { + if (instanceQuery.isFetching) { + return ; + } + + let data = manufacturerPart ?? {}; + + let tl: DetailsField[] = [ + { + type: 'link', + name: 'part', + label: t`Internal Part`, + model: ModelType.part, + hidden: !manufacturerPart.part + }, + { + type: 'string', + name: 'description', + label: t`Description`, + copy: true, + hidden: !manufacturerPart.description + }, + { + type: 'link', + external: true, + name: 'link', + label: t`External Link`, + copy: true, + hidden: !manufacturerPart.link + } + ]; + + let 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' + } + ]; + + return ( + + + + + + + + + + + + ); + }, [manufacturerPart, instanceQuery]); + const panels: PanelType[] = useMemo(() => { return [ { name: 'details', - label: t`Details`, - icon: + label: t`Manufacturer Part Details`, + icon: , + content: detailsPanel }, { name: 'parameters', @@ -78,6 +178,38 @@ export default function ManufacturerPartDetail() { ]; }, [manufacturerPart]); + const editManufacturerPartFields = useManufacturerPartFields(); + + const editManufacturerPart = useEditApiFormModal({ + url: ApiEndpoints.manufacturer_part_list, + pk: manufacturerPart?.pk, + title: t`Edit Manufacturer Part`, + fields: editManufacturerPartFields, + onFormSuccess: refreshInstance + }); + + const manufacturerPartActions = useMemo(() => { + return [ + } + actions={[ + DuplicateItemAction({ + hidden: !user.hasAddRole(UserRoles.purchase_order) + }), + EditItemAction({ + hidden: !user.hasChangeRole(UserRoles.purchase_order), + onClick: () => editManufacturerPart.open() + }), + DeleteItemAction({ + hidden: !user.hasDeleteRole(UserRoles.purchase_order) + }) + ]} + /> + ]; + }, [user]); + const breadcrumbs = useMemo(() => { return [ { @@ -92,15 +224,19 @@ export default function ManufacturerPartDetail() { }, [manufacturerPart]); return ( - - - - - + <> + {editManufacturerPart.modal} + + + + + + ); } diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index 54b0c652c6..9c98f2c02d 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -1,7 +1,8 @@ import { t } from '@lingui/macro'; -import { LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { IconCurrencyDollar, + IconDots, IconInfoCircle, IconPackages, IconShoppingCart @@ -9,16 +10,37 @@ import { import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import { DetailsField, DetailsTable } from '../../components/details/Details'; +import { DetailsImage } from '../../components/details/DetailsImage'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; +import { + ActionDropdown, + DeleteItemAction, + DuplicateItemAction, + EditItemAction +} from '../../components/items/ActionDropdown'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useSupplierPartFields } from '../../forms/CompanyForms'; +import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable'; export default function SupplierPartDetail() { const { id } = useParams(); - const { instance: supplierPart, instanceQuery } = useInstance({ + const user = useUserState(); + + const { + instance: supplierPart, + instanceQuery, + refreshInstance + } = useInstance({ endpoint: ApiEndpoints.supplier_part_list, pk: id, hasPrimaryKey: true, @@ -28,12 +50,153 @@ export default function SupplierPartDetail() { } }); + const detailsPanel = useMemo(() => { + if (instanceQuery.isFetching) { + return ; + } + + let data = supplierPart ?? {}; + + // Access nested data + data.manufacturer = data.manufacturer_detail?.pk; + data.MPN = data.manufacturer_part_detail?.MPN; + data.manufacturer_part = data.manufacturer_part_detail?.pk; + + let tl: DetailsField[] = [ + { + type: 'link', + name: 'part', + label: t`Internal Part`, + model: ModelType.part, + hidden: !supplierPart.part + }, + { + type: 'string', + name: 'description', + label: t`Description`, + copy: true + }, + { + 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 + } + ]; + + let tr: 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: '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 Number`, + model: ModelType.manufacturerpart, + copy: true, + icon: 'reference', + hidden: !data.manufacturer_part + } + ]; + + let bl: 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' + } + ]; + + let br: DetailsField[] = [ + { + type: 'string', + name: 'available', + label: t`Supplier Availability`, + copy: true, + icon: 'packages' + }, + { + type: 'string', + 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`Details`, - icon: + label: t`Supplier Part Details`, + icon: , + content: detailsPanel }, { name: 'stock', @@ -58,6 +221,41 @@ export default function SupplierPartDetail() { ]; }, [supplierPart]); + const supplierPartActions = useMemo(() => { + return [ + } + actions={[ + DuplicateItemAction({ + hidden: !user.hasAddRole(UserRoles.purchase_order) + }), + EditItemAction({ + hidden: !user.hasChangeRole(UserRoles.purchase_order), + onClick: () => editSuppliertPart.open() + }), + DeleteItemAction({ + hidden: !user.hasDeleteRole(UserRoles.purchase_order) + }) + ]} + /> + ]; + }, [user]); + + const editSupplierPartFields = useSupplierPartFields({ + hidePart: true, + partPk: supplierPart?.pk + }); + + const editSuppliertPart = useEditApiFormModal({ + url: ApiEndpoints.supplier_part_list, + pk: supplierPart?.pk, + title: t`Edit Supplier Part`, + fields: editSupplierPartFields, + onFormSuccess: refreshInstance + }); + const breadcrumbs = useMemo(() => { return [ { @@ -72,15 +270,19 @@ export default function SupplierPartDetail() { }, [supplierPart]); return ( - - - - - + <> + {editSuppliertPart.modal} + + + + + + ); } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 08c37bb79a..155d6f4148 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -655,7 +655,6 @@ export default function PartDetail() { const transferStockItems = useTransferStockItem(stockActionProps); const partActions = useMemo(() => { - // TODO: Disable actions based on user permissions return [ { part.pk && countStockItems.open(); } @@ -689,6 +689,7 @@ export default function PartDetail() { ), name: t`Transfer Stock`, tooltip: t`Transfer part stock`, + hidden: !user.hasChangeRole(UserRoles.stock), onClick: () => { part.pk && transferStockItems.open(); } @@ -700,13 +701,15 @@ export default function PartDetail() { tooltip={t`Part Actions`} icon={} actions={[ - DuplicateItemAction({}), + DuplicateItemAction({ + hidden: !user.hasAddRole(UserRoles.part) + }), EditItemAction({ hidden: !user.hasChangeRole(UserRoles.part), onClick: () => editPart.open() }), DeleteItemAction({ - hidden: part?.active + hidden: part?.active || !user.hasDeleteRole(UserRoles.part) }) ]} /> diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx index 93ff5be477..63bdd40fe0 100644 --- a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx +++ b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx @@ -11,6 +11,7 @@ import { useManufacturerPartFields } from '../../forms/CompanyForms'; import { openDeleteApiForm, openEditApiForm } from '../../functions/forms'; import { notYetImplemented } from '../../functions/notifications'; import { getDetailUrl } from '../../functions/urls'; +import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -61,9 +62,15 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode { ]; }, [params]); - const addManufacturerPart = useCallback(() => { - notYetImplemented(); - }, []); + const createManufacturerPart = useCreateApiFormModal({ + url: ApiEndpoints.manufacturer_part_list, + title: t`Create Manufacturer Part`, + fields: useManufacturerPartFields(), + onFormSuccess: table.refreshTable, + initialData: { + manufacturer: params?.manufacturer + } + }); const tableActions = useMemo(() => { let can_add = @@ -73,7 +80,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode { return [ createManufacturerPart.open()} hidden={!can_add} /> ]; @@ -118,24 +125,27 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode { ); return ( - { - if (record?.pk) { - navigate(getDetailUrl(ModelType.manufacturerpart, record.pk)); + <> + {createManufacturerPart.modal} + { + if (record?.pk) { + navigate(getDetailUrl(ModelType.manufacturerpart, record.pk)); + } } - } - }} - /> + }} + /> + ); }