diff --git a/src/frontend/src/components/buttons/ActionButton.tsx b/src/frontend/src/components/buttons/ActionButton.tsx new file mode 100644 index 0000000000..9aeb65cb00 --- /dev/null +++ b/src/frontend/src/components/buttons/ActionButton.tsx @@ -0,0 +1,44 @@ +import { ActionIcon, Group, Tooltip } from '@mantine/core'; +import { ReactNode } from 'react'; + +import { notYetImplemented } from '../../functions/notifications'; + +export type ActionButtonProps = { + icon?: ReactNode; + text?: string; + color?: string; + tooltip?: string; + variant?: string; + size?: number; + disabled?: boolean; + onClick?: any; + hidden?: boolean; +}; + +/** + * Construct a simple action button with consistent styling + */ +export function ActionButton(props: ActionButtonProps) { + return ( + !props.hidden && ( + + + + {props.icon} + + + + ) + ); +} diff --git a/src/frontend/src/components/buttons/AddItemButton.tsx b/src/frontend/src/components/buttons/AddItemButton.tsx new file mode 100644 index 0000000000..a9af3de8f5 --- /dev/null +++ b/src/frontend/src/components/buttons/AddItemButton.tsx @@ -0,0 +1,10 @@ +import { IconPlus } from '@tabler/icons-react'; + +import { ActionButton, ActionButtonProps } from './ActionButton'; + +/** + * A generic icon button which is used to add or create a new item + */ +export function AddItemButton(props: ActionButtonProps) { + return } />; +} diff --git a/src/frontend/src/components/items/ButtonMenu.tsx b/src/frontend/src/components/buttons/ButtonMenu.tsx similarity index 100% rename from src/frontend/src/components/items/ButtonMenu.tsx rename to src/frontend/src/components/buttons/ButtonMenu.tsx diff --git a/src/frontend/src/components/items/ActionButton.tsx b/src/frontend/src/components/items/ActionButton.tsx deleted file mode 100644 index 48d7cd9b2a..0000000000 --- a/src/frontend/src/components/items/ActionButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ActionIcon, Tooltip } from '@mantine/core'; - -/** - * Construct a simple action button with consistent styling - */ -export function ActionButton({ - icon, - color = 'black', - tooltip = '', - disabled = false, - size = 18, - onClick -}: { - icon: any; - color?: string; - tooltip?: string; - variant?: string; - size?: number; - disabled?: boolean; - onClick?: any; -}) { - return ( - - - {icon} - - - ); -} diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index e8b937fcac..b5b9ce17c7 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -1,6 +1,13 @@ import { t } from '@lingui/macro'; import { ActionIcon, Menu, Tooltip } from '@mantine/core'; -import { IconQrcode } from '@tabler/icons-react'; +import { + IconCopy, + IconEdit, + IconLink, + IconQrcode, + IconTrash, + IconUnlink +} from '@tabler/icons-react'; import { ReactNode, useMemo } from 'react'; import { notYetImplemented } from '../../functions/notifications'; @@ -81,3 +88,111 @@ export function BarcodeActionDropdown({ /> ); } + +// Common action button for viewing a barcode +export function ViewBarcodeAction({ + disabled = false, + callback +}: { + disabled?: boolean; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`View`, + tooltip: t`View barcode`, + onClick: callback, + disabled: disabled + }; +} + +// Common action button for linking a custom barcode +export function LinkBarcodeAction({ + disabled = false, + callback +}: { + disabled?: boolean; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Link Barcode`, + tooltip: t`Link custom barcode`, + onClick: callback, + disabled: disabled + }; +} + +// Common action button for un-linking a custom barcode +export function UnlinkBarcodeAction({ + disabled = false, + callback +}: { + disabled?: boolean; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Unlink Barcode`, + tooltip: t`Unlink custom barcode`, + onClick: callback, + disabled: disabled + }; +} + +// Common action button for editing an item +export function EditItemAction({ + disabled = false, + tooltip, + callback +}: { + disabled?: boolean; + tooltip?: string; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Edit`, + tooltip: tooltip ?? `Edit item`, + onClick: callback, + disabled: disabled + }; +} + +// Common action button for deleting an item +export function DeleteItemAction({ + disabled = false, + tooltip, + callback +}: { + disabled?: boolean; + tooltip?: string; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Delete`, + tooltip: tooltip ?? t`Delete item`, + onClick: callback, + disabled: disabled + }; +} + +// Common action button for duplicating an item +export function DuplicateItemAction({ + disabled = false, + tooltip, + callback +}: { + disabled?: boolean; + tooltip?: string; + callback?: () => void; +}): ActionDropdownItem { + return { + icon: , + name: t`Duplicate`, + tooltip: tooltip ?? t`Duplicate item`, + onClick: callback, + disabled: disabled + }; +} diff --git a/src/frontend/src/components/items/ProgressBar.tsx b/src/frontend/src/components/items/ProgressBar.tsx new file mode 100644 index 0000000000..186051b1b5 --- /dev/null +++ b/src/frontend/src/components/items/ProgressBar.tsx @@ -0,0 +1,39 @@ +import { Progress, Stack, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +export type ProgressBarProps = { + value: number; + maximum?: number; + label?: string; + progressLabel?: boolean; +}; + +/** + * A progress bar element, built on mantine.Progress + * The color of the bar is determined based on the value + */ +export function ProgressBar(props: ProgressBarProps) { + const progress = useMemo(() => { + let maximum = props.maximum ?? 100; + let value = Math.max(props.value, 0); + + // Calculate progress as a percentage of the maximum value + return Math.min(100, (value / maximum) * 100); + }, [props]); + + return ( + + {props.progressLabel && ( + + {props.value} / {props.maximum} + + )} + 100 ? 'blue' : 'green'} + size="sm" + radius="xs" + /> + + ); +} diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index a1d2b40e87..23771f0ec2 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -52,19 +52,19 @@ export function RenderContact({ instance }: { instance: any }): ReactNode { * Inline rendering of a single SupplierPart instance */ export function RenderSupplierPart({ instance }: { instance: any }): ReactNode { - // TODO: Handle image // TODO: handle URL let supplier = instance.supplier_detail ?? {}; let part = instance.part_detail ?? {}; - let text = instance.SKU; - - if (supplier.name) { - text = `${supplier.name} | ${text}`; - } - - return ; + return ( + + ); } /** diff --git a/src/frontend/src/components/render/User.tsx b/src/frontend/src/components/render/User.tsx index 6bad2a3011..8f77476cde 100644 --- a/src/frontend/src/components/render/User.tsx +++ b/src/frontend/src/components/render/User.tsx @@ -5,14 +5,16 @@ import { RenderInlineModel } from './Instance'; export function RenderOwner({ instance }: { instance: any }): ReactNode { // TODO: Icon based on user / group status? - return ; + return instance && ; } export function RenderUser({ instance }: { instance: any }): ReactNode { return ( - + instance && ( + + ) ); } diff --git a/src/frontend/src/components/renderers/PurchaseOrderRenderer.tsx b/src/frontend/src/components/renderers/PurchaseOrderRenderer.tsx index 1b0272552b..d59b6789e1 100644 --- a/src/frontend/src/components/renderers/PurchaseOrderRenderer.tsx +++ b/src/frontend/src/components/renderers/PurchaseOrderRenderer.tsx @@ -17,7 +17,7 @@ export const PurchaseOrderRenderer = ({ pk }: { pk: string }) => { return ( {t`Select Columns`} {columns - .filter((col) => col.switchable) + .filter((col) => col.switchable ?? true) .map((col) => ( [], onRowClick: (record: any, index: number, event: any) => {} }; @@ -115,7 +114,7 @@ export function InvenTreeTable({ // Check if any columns are switchable (can be hidden) const hasSwitchableColumns = columns.some( - (col: TableColumn) => col.switchable + (col: TableColumn) => col.switchable ?? true ); // A list of hidden columns, saved to local storage @@ -142,7 +141,7 @@ export function InvenTreeTable({ let cols = columns.map((col) => { let hidden: boolean = col.hidden ?? false; - if (col.switchable) { + if (col.switchable ?? true) { hidden = hiddenColumns.includes(col.accessor); } @@ -156,10 +155,19 @@ export function InvenTreeTable({ if (tableProps.rowActions) { cols.push({ accessor: 'actions', - title: '', + title: ' ', hidden: false, switchable: false, - width: 48, + width: 50, + cellsStyle: { + position: 'sticky', + right: 0, + // TODO: Use the theme color to set the background color + backgroundColor: '#FFF', + // TODO: Use the scroll area callbacks to determine if we need to display a "shadow" + borderLeft: '1px solid #DDD', + padding: '3px' + }, render: function (record: any) { return ( )} - {tableProps.enableDownload && ( - - )} @@ -488,6 +490,12 @@ export function InvenTreeTable({ )} + {tableProps.enableDownload && ( + + )} {filtersVisible && ( @@ -504,7 +512,7 @@ export function InvenTreeTable({ highlightOnHover loaderVariant="dots" idAccessor={tableProps.idAccessor} - minHeight={200} + minHeight={300} totalRecords={recordCount} recordsPerPage={tableProps.pageSize ?? defaultPageSize} page={page} diff --git a/src/frontend/src/components/tables/RowActions.tsx b/src/frontend/src/components/tables/RowActions.tsx index 1c0392c6ab..a16de60c17 100644 --- a/src/frontend/src/components/tables/RowActions.tsx +++ b/src/frontend/src/components/tables/RowActions.tsx @@ -15,7 +15,23 @@ export type RowAction = { hidden?: boolean; }; -// Component for ediitng a row in a table +// Component for duplicating a row in a table +export function RowDuplicateAction({ + onClick, + hidden +}: { + onClick?: () => void; + hidden?: boolean; +}): RowAction { + return { + title: t`Duplicate`, + color: 'green', + onClick: onClick, + hidden: hidden + }; +} + +// Component for editing a row in a table export function RowEditAction({ onClick, hidden @@ -25,7 +41,7 @@ export function RowEditAction({ }): RowAction { return { title: t`Edit`, - color: 'green', + color: 'blue', onClick: onClick, hidden: hidden }; diff --git a/src/frontend/src/components/tables/TableHoverCard.tsx b/src/frontend/src/components/tables/TableHoverCard.tsx index d91fe3407a..03499f585d 100644 --- a/src/frontend/src/components/tables/TableHoverCard.tsx +++ b/src/frontend/src/components/tables/TableHoverCard.tsx @@ -1,5 +1,6 @@ -import { Divider, Group, HoverCard, Stack } from '@mantine/core'; +import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core'; import { IconInfoCircle } from '@tabler/icons-react'; +import { ReactNode } from 'react'; /* * A custom hovercard element for displaying extra information in a table cell. @@ -12,7 +13,7 @@ export function TableHoverCard({ title // The title of the hovercard }: { value: any; - extra?: any; + extra?: ReactNode; title?: string; }) { // If no extra information presented, just return the raw value @@ -32,7 +33,7 @@ export function TableHoverCard({ - {title} + {title} {extra} diff --git a/src/frontend/src/components/tables/bom/BomTable.tsx b/src/frontend/src/components/tables/bom/BomTable.tsx index cf05870946..44db315b10 100644 --- a/src/frontend/src/components/tables/bom/BomTable.tsx +++ b/src/frontend/src/components/tables/bom/BomTable.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Stack, Text } from '@mantine/core'; +import { Text } from '@mantine/core'; import { ReactNode, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -51,12 +51,11 @@ export function BomTable({ { accessor: 'description', title: t`Description`, - switchable: true, render: (row) => row?.sub_part_detail?.description }, { accessor: 'reference', - switchable: true, + title: t`Reference` }, { @@ -66,7 +65,7 @@ export function BomTable({ { accessor: 'substitutes', title: t`Substitutes`, - switchable: true, + render: (row) => { let substitutes = row.substitutes ?? []; @@ -80,7 +79,7 @@ export function BomTable({ { accessor: 'optional', title: t`Optional`, - switchable: true, + sortable: true, render: (row) => { return ; @@ -89,7 +88,7 @@ export function BomTable({ { accessor: 'consumable', title: t`Consumable`, - switchable: true, + sortable: true, render: (row) => { return ; @@ -98,7 +97,7 @@ export function BomTable({ { accessor: 'allow_variants', title: t`Allow Variants`, - switchable: true, + sortable: true, render: (row) => { return ; @@ -107,7 +106,7 @@ export function BomTable({ { accessor: 'inherited', title: t`Gets Inherited`, - switchable: true, + sortable: true, render: (row) => { // TODO: Update complexity here @@ -117,7 +116,7 @@ export function BomTable({ { accessor: 'price_range', title: t`Price Range`, - switchable: true, + sortable: false, render: (row) => { let min_price = row.pricing_min || row.pricing_max; @@ -130,7 +129,7 @@ export function BomTable({ { accessor: 'available_stock', title: t`Available`, - switchable: true, + render: (row) => { let extra: ReactNode[] = []; @@ -164,9 +163,7 @@ export function BomTable({ return ( 0 ? {extra} : null - } + extra={extra} title={t`Available Stock`} /> ); @@ -175,7 +172,7 @@ export function BomTable({ { accessor: 'can_build', title: t`Can Build`, - switchable: true, + sortable: true // TODO: Custom sorting via API // TODO: Reference bom.js for canBuildQuantity method }, diff --git a/src/frontend/src/components/tables/bom/UsedInTable.tsx b/src/frontend/src/components/tables/bom/UsedInTable.tsx index 02886df668..486c69e704 100644 --- a/src/frontend/src/components/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/components/tables/bom/UsedInTable.tsx @@ -73,8 +73,7 @@ export function UsedInTable({ { accessor: 'reference', title: t`Reference`, - sortable: true, - switchable: true + sortable: true } ]; }, [partId]); diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index e106cc5cde..2de0e78ad3 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -1,16 +1,19 @@ import { t } from '@lingui/macro'; -import { Progress } from '@mantine/core'; +import { Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ThumbnailHoverCard } from '../../images/Thumbnail'; +import { ProgressBar } from '../../items/ProgressBar'; import { ModelType } from '../../render/ModelType'; +import { RenderOwner, RenderUser } from '../../render/User'; import { TableStatusRenderer } from '../../renderers/StatusRenderer'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; +import { TableHoverCard } from '../TableHoverCard'; /** * Construct a list of columns for the build order table @@ -20,12 +23,13 @@ function buildOrderTableColumns(): TableColumn[] { { accessor: 'reference', sortable: true, + switchable: false, title: t`Reference` - // TODO: Link to the build order detail page }, { accessor: 'part', sortable: true, + switchable: false, title: t`Part`, render: (record: any) => { let part = record.part_detail; @@ -44,86 +48,87 @@ function buildOrderTableColumns(): TableColumn[] { { accessor: 'title', sortable: false, - title: t`Description`, - switchable: true - }, - { - accessor: 'project_code', - title: t`Project Code`, - sortable: true, - switchable: false, - hidden: true - // TODO: Hide this if project code is not enabled - // TODO: Custom render function here + title: t`Description` }, { accessor: 'quantity', sortable: true, title: t`Quantity`, - switchable: true + switchable: false }, { accessor: 'completed', sortable: true, title: t`Completed`, - render: (record: any) => { - let progress = - record.quantity <= 0 ? 0 : (100 * record.completed) / record.quantity; - return ( - - ); - } + render: (record: any) => ( + + ) }, { accessor: 'status', sortable: true, title: t`Status`, - switchable: true, + render: TableStatusRenderer(ModelType.build) }, + { + accessor: 'project_code', + title: t`Project Code`, + sortable: true, + // TODO: Hide this if project code is not enabled + render: (record: any) => { + let project = record.project_code_detail; + + return project ? ( + {project.description}} + /> + ) : ( + '-' + ); + } + }, { accessor: 'priority', title: t`Priority`, - sortable: true, - switchable: true + sortable: true }, { accessor: 'creation_date', sortable: true, - title: t`Created`, - switchable: true + title: t`Created` }, { accessor: 'target_date', sortable: true, - title: t`Target Date`, - switchable: true + title: t`Target Date` }, { accessor: 'completion_date', sortable: true, - title: t`Completed`, - switchable: true + title: t`Completed` }, { accessor: 'issued_by', sortable: true, title: t`Issued By`, - switchable: true - // TODO: custom render function + render: (record: any) => ( + + ) }, { accessor: 'responsible', sortable: true, title: t`Responsible`, - switchable: true - // TODO: custom render function + render: (record: any) => ( + + ) } ]; } diff --git a/src/frontend/src/components/tables/general/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx index f8b9dc89c5..5519e0870a 100644 --- a/src/frontend/src/components/tables/general/AttachmentTable.tsx +++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx @@ -45,7 +45,7 @@ function attachmentTableColumns(): TableColumn[] { accessor: 'comment', title: t`Comment`, sortable: false, - switchable: true, + render: function (record: any) { return record.comment; } @@ -54,7 +54,7 @@ function attachmentTableColumns(): TableColumn[] { accessor: 'uploaded', title: t`Uploaded`, sortable: false, - switchable: true, + render: function (record: any) { return ( diff --git a/src/frontend/src/components/tables/general/CompanyTable.tsx b/src/frontend/src/components/tables/general/CompanyTable.tsx index 73a40c1b12..77908caea4 100644 --- a/src/frontend/src/components/tables/general/CompanyTable.tsx +++ b/src/frontend/src/components/tables/general/CompanyTable.tsx @@ -45,14 +45,12 @@ export function CompanyTable({ { accessor: 'description', title: t`Description`, - sortable: false, - switchable: true + sortable: false }, { accessor: 'website', title: t`Website`, - sortable: false, - switchable: true + sortable: false } ]; }, []); diff --git a/src/frontend/src/components/tables/part/PartCategoryTable.tsx b/src/frontend/src/components/tables/part/PartCategoryTable.tsx index a5d14a2dea..c2c7ad55ed 100644 --- a/src/frontend/src/components/tables/part/PartCategoryTable.tsx +++ b/src/frontend/src/components/tables/part/PartCategoryTable.tsx @@ -26,20 +26,17 @@ export function PartCategoryTable({ params = {} }: { params?: any }) { { accessor: 'description', title: t`Description`, - sortable: false, - switchable: true + sortable: false }, { accessor: 'pathstring', title: t`Path`, - sortable: false, - switchable: true + sortable: false }, { accessor: 'part_count', title: t`Parts`, - sortable: true, - switchable: true + sortable: true } ]; }, []); diff --git a/src/frontend/src/components/tables/part/PartParameterTable.tsx b/src/frontend/src/components/tables/part/PartParameterTable.tsx index 2823a45791..855f00e763 100644 --- a/src/frontend/src/components/tables/part/PartParameterTable.tsx +++ b/src/frontend/src/components/tables/part/PartParameterTable.tsx @@ -1,6 +1,5 @@ import { t } from '@lingui/macro'; -import { ActionIcon, Group, Text, Tooltip } from '@mantine/core'; -import { IconTextPlus } from '@tabler/icons-react'; +import { Group, Text } from '@mantine/core'; import { useCallback, useMemo } from 'react'; import { @@ -10,6 +9,7 @@ import { } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { AddItemButton } from '../../buttons/AddItemButton'; import { Thumbnail } from '../../images/Thumbnail'; import { YesNoButton } from '../../items/YesNoButton'; import { TableColumn } from '../Column'; @@ -27,7 +27,7 @@ export function PartParameterTable({ partId }: { partId: any }) { { accessor: 'part', title: t`Part`, - switchable: true, + sortable: true, render: function (record: any) { let part = record?.part_detail ?? {}; @@ -59,7 +59,7 @@ export function PartParameterTable({ partId }: { partId: any }) { accessor: 'description', title: t`Description`, sortable: false, - switchable: true, + render: (record) => record.template_detail?.description }, { @@ -86,7 +86,7 @@ export function PartParameterTable({ partId }: { partId: any }) { { accessor: 'units', title: t`Units`, - switchable: true, + sortable: true, render: (record) => record.template_detail?.units } @@ -174,11 +174,7 @@ export function PartParameterTable({ partId }: { partId: any }) { // TODO: Hide if user does not have permission to edit parts actions.push( - - - - - + ); return actions; diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx index fb2466d1d8..49221ce43a 100644 --- a/src/frontend/src/components/tables/part/PartTable.tsx +++ b/src/frontend/src/components/tables/part/PartTable.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Group, Stack, Text } from '@mantine/core'; +import { Group, Text } from '@mantine/core'; import { ReactNode, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -35,26 +35,23 @@ function partTableColumns(): TableColumn[] { { accessor: 'IPN', title: t`IPN`, - sortable: true, - switchable: true + sortable: true }, { accessor: 'units', sortable: true, - title: t`Units`, - switchable: true + title: t`Units` }, { accessor: 'description', title: t`Description`, - sortable: true, - switchable: true + sortable: true }, { accessor: 'category', title: t`Category`, sortable: true, - switchable: true, + render: function (record: any) { // TODO: Link to the category detail page return shortenString({ @@ -66,7 +63,7 @@ function partTableColumns(): TableColumn[] { accessor: 'total_in_stock', title: t`Stock`, sortable: true, - switchable: true, + render: (record) => { let extra: ReactNode[] = []; @@ -143,7 +140,7 @@ function partTableColumns(): TableColumn[] { } title={t`Stock Information`} - extra={extra.length > 0 && {extra}} + extra={extra} /> ); } @@ -152,7 +149,7 @@ function partTableColumns(): TableColumn[] { accessor: 'price_range', title: t`Price Range`, sortable: false, - switchable: true, + render: function (record: any) { // TODO: Render price range return '-- price --'; @@ -160,8 +157,7 @@ function partTableColumns(): TableColumn[] { }, { accessor: 'link', - title: t`Link`, - switchable: true + title: t`Link` } ]; } diff --git a/src/frontend/src/components/tables/plugin/PluginListTable.tsx b/src/frontend/src/components/tables/plugin/PluginListTable.tsx index 0c171c58b7..20a85cce0e 100644 --- a/src/frontend/src/components/tables/plugin/PluginListTable.tsx +++ b/src/frontend/src/components/tables/plugin/PluginListTable.tsx @@ -68,7 +68,7 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) { accessor: 'meta.description', title: t`Description`, sortable: false, - switchable: true, + render: function (record: any) { if (record.active) { return record.meta.description; @@ -80,15 +80,14 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) { { accessor: 'meta.version', title: t`Version`, - sortable: false, - switchable: true + sortable: false + // TODO: Display date information if available }, { accessor: 'meta.author', title: 'Author', - sortable: false, - switchable: true + sortable: false } ], [] diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx new file mode 100644 index 0000000000..bd264a1ba8 --- /dev/null +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -0,0 +1,266 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { IconSquareArrowRight } from '@tabler/icons-react'; +import { useCallback, useMemo } from 'react'; + +import { ProgressBar } from '../../../components/items/ProgressBar'; +import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms'; +import { openCreateApiForm, openEditApiForm } from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; +import { ActionButton } from '../../buttons/ActionButton'; +import { AddItemButton } from '../../buttons/AddItemButton'; +import { Thumbnail } from '../../images/Thumbnail'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { + RowDeleteAction, + RowDuplicateAction, + RowEditAction +} from '../RowActions'; +import { TableHoverCard } from '../TableHoverCard'; + +/* + * Display a table of purchase order line items, for a specific order + */ +export function PurchaseOrderLineItemTable({ + orderId, + params +}: { + orderId: number; + params?: any; +}) { + const { tableKey, refreshTable } = useTableRefresh( + 'purchase-order-line-item' + ); + + const user = useUserState(); + + const rowActions = useCallback( + (record: any) => { + // TODO: Hide certain actions if user does not have required permissions + + let received = (record?.received ?? 0) >= (record?.quantity ?? 0); + + return [ + { + hidden: received, + title: t`Receive`, + tooltip: t`Receive line item`, + color: 'green' + }, + RowEditAction({ + onClick: () => { + let supplier = record?.supplier_part_detail?.supplier; + + if (!supplier) { + return; + } + + let fields = purchaseOrderLineItemFields({ + supplierId: supplier + }); + + openEditApiForm({ + url: ApiPaths.purchase_order_line_list, + pk: record.pk, + title: t`Edit Line Item`, + fields: fields, + onFormSuccess: refreshTable, + successMessage: t`Line item updated` + }); + } + }), + RowDuplicateAction({}), + RowDeleteAction({}) + ]; + }, + [orderId, user] + ); + + const tableColumns = useMemo(() => { + return [ + { + accessor: 'part', + title: t`Part`, + sortable: true, + switchable: false, + render: (record: any) => { + return ( + + ); + } + }, + { + accessor: 'description', + title: t`Part Description`, + + sortable: false, + render: (record: any) => record?.part_detail?.description + }, + { + accessor: 'reference', + title: t`Reference`, + sortable: true + }, + { + accessor: 'quantity', + title: t`Quantity`, + sortable: true, + switchable: false, + render: (record: any) => { + let part = record?.part_detail; + let supplier_part = record?.supplier_part_detail ?? {}; + let extra = []; + + if (supplier_part.pack_quantity_native != 1) { + let total = record.quantity * supplier_part.pack_quantity_native; + + extra.push( + + {t`Pack Quantity`}: {supplier_part.pack_quantity} + + ); + + extra.push( + + {t`Total Quantity`}: {total} + {part.units} + + ); + } + + return ( + + ); + } + }, + { + accessor: 'received', + title: t`Received`, + sortable: false, + + render: (record: any) => ( + + ) + }, + { + accessor: 'pack_quantity', + sortable: false, + + title: t`Pack Quantity`, + render: (record: any) => record?.supplier_part_detail?.pack_quantity + }, + { + accessor: 'SKU', + title: t`Supplier Code`, + switchable: false, + sortable: true + }, + { + accessor: 'supplier_link', + title: t`Supplier Link`, + + sortable: false, + render: (record: any) => record?.supplier_part_detail?.link + }, + { + accessor: 'MPN', + title: t`Manufacturer Code`, + sortable: true, + + render: (record: any) => + record?.supplier_part_detail?.manufacturer_part_detail?.MPN + }, + + { + accessor: 'purchase_price', + title: t`Unit Price`, + sortable: true + + // TODO: custom renderer + }, + { + accessor: 'total_price', + title: t`Total Price`, + sortable: true + + // TODO: custom renderer + }, + { + accessor: 'target_date', + title: t`Target Date`, + sortable: true + }, + { + accessor: 'destination', + title: t`Destination`, + sortable: false + + // TODO: Custom renderer + }, + { + accessor: 'notes', + title: t`Notes` + }, + { + accessor: 'link', + title: t`Link` + + // TODO: custom renderer + } + ]; + }, [orderId, user]); + + const addLine = useCallback(() => { + openCreateApiForm({ + url: ApiPaths.purchase_order_line_list, + title: t`Add Line Item`, + fields: purchaseOrderLineItemFields({}), + onFormSuccess: refreshTable, + successMessage: t`Line item added` + }); + }, []); + + // Custom table actions + const tableActions = useMemo(() => { + return [ +