From e5fabc6788d3515f4c9fc6cbe252b5e9d2efc394 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 2 Aug 2024 09:13:50 +1000 Subject: [PATCH] [PUI] Table Updates (#7783) * Refactor part display in tables - Remove legacy code * Remove unused function * Refactoring for PurchaseOrderLineItemTable * Implement sales order line item table * Add placeholders for row actions * Implement table actions * Add placeholder action to allocate stock --- .../src/components/images/Thumbnail.tsx | 46 --- src/frontend/src/forms/SalesOrderForms.tsx | 37 +++ .../src/pages/sales/SalesOrderDetail.tsx | 8 +- src/frontend/src/tables/bom/UsedInTable.tsx | 9 +- .../src/tables/build/BuildLineTable.tsx | 5 +- .../src/tables/build/BuildOrderTable.tsx | 4 +- .../purchasing/PurchaseOrderLineItemTable.tsx | 11 +- .../tables/sales/SalesOrderLineItemTable.tsx | 272 ++++++++++++++++++ 8 files changed, 327 insertions(+), 65 deletions(-) create mode 100644 src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx diff --git a/src/frontend/src/components/images/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx index 2b63b0f16a..63e2414122 100644 --- a/src/frontend/src/components/images/Thumbnail.tsx +++ b/src/frontend/src/components/images/Thumbnail.tsx @@ -50,49 +50,3 @@ export function Thumbnail({ ); } - -export function ThumbnailHoverCard({ - src, - text, - link = '', - alt = t`Thumbnail`, - size = 20 -}: { - src: string; - text: string; - link?: string; - alt?: string; - size?: number; -}) { - const card = useMemo(() => { - return ( - - - {text} - - ); - }, [src, text, alt, size]); - - if (link) { - return ( - - {card} - - ); - } - - return
{card}
; -} - -export function PartHoverCard({ part }: { part: any }) { - return part ? ( - - ) : ( - - ); -} diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 9c97f13201..9c11eb5c50 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -47,6 +47,43 @@ export function useSalesOrderFields(): ApiFormFieldSet { }, []); } +export function useSalesOrderLineItemFields({ + customerId, + orderId, + create +}: { + customerId?: number; + orderId?: number; + create?: boolean; +}): ApiFormFieldSet { + const fields = useMemo(() => { + return { + order: { + filters: { + customer_detail: true + }, + disabled: true, + value: create ? orderId : undefined + }, + part: { + filters: { + active: true, + salable: true + } + }, + reference: {}, + quantity: {}, + sale_price: {}, + sale_price_currency: {}, + target_date: {}, + notes: {}, + link: {} + }; + }, []); + + return fields; +} + export function useReturnOrderFields(): ApiFormFieldSet { return useMemo(() => { return { diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 967749a5c4..5fb6c861c6 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -47,6 +47,7 @@ import { useInstance } from '../../hooks/UseInstance'; import { useUserState } from '../../states/UserState'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; +import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable'; /** * Detail page for a single SalesOrder @@ -249,7 +250,12 @@ export default function SalesOrderDetail() { name: 'line-items', label: t`Line Items`, icon: , - content: + content: ( + + ) }, { name: 'pending-shipments', diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx index f9227f92e2..bd0e7d8e14 100644 --- a/src/frontend/src/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/tables/bom/UsedInTable.tsx @@ -2,14 +2,13 @@ import { t } from '@lingui/macro'; import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; -import { PartHoverCard } from '../../components/images/Thumbnail'; import { formatDecimal } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { TableColumn } from '../Column'; -import { ReferenceColumn } from '../ColumnRenderers'; +import { PartColumn, ReferenceColumn } from '../ColumnRenderers'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -31,12 +30,14 @@ export function UsedInTable({ accessor: 'part', switchable: false, sortable: true, - render: (record: any) => + title: t`Assembly`, + render: (record: any) => PartColumn(record.part_detail) }, { accessor: 'sub_part', sortable: true, - render: (record: any) => + title: t`Component`, + render: (record: any) => PartColumn(record.sub_part_detail) }, { accessor: 'quantity', diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 90ee524b51..5d0b6c90ac 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -7,7 +7,6 @@ import { } from '@tabler/icons-react'; import { useCallback, useMemo } from 'react'; -import { PartHoverCard } from '../../components/images/Thumbnail'; import { ProgressBar } from '../../components/items/ProgressBar'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; @@ -15,7 +14,7 @@ import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; -import { BooleanColumn } from '../ColumnRenderers'; +import { BooleanColumn, PartColumn } from '../ColumnRenderers'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; import { TableHoverCard } from '../TableHoverCard'; @@ -131,7 +130,7 @@ export default function BuildLineTable({ params = {} }: { params?: any }) { ordering: 'part', sortable: true, switchable: false, - render: (record: any) => + render: (record: any) => PartColumn(record.part_detail) }, { accessor: 'bom_item_detail.reference', diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 868840f551..18ff6f4f05 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -2,7 +2,6 @@ import { t } from '@lingui/macro'; import { useMemo } from 'react'; import { AddItemButton } from '../../components/buttons/AddItemButton'; -import { PartHoverCard } from '../../components/images/Thumbnail'; import { ProgressBar } from '../../components/items/ProgressBar'; import { RenderUser } from '../../components/render/User'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; @@ -22,6 +21,7 @@ import { TableColumn } from '../Column'; import { CreationDateColumn, DateColumn, + PartColumn, ProjectCodeColumn, ReferenceColumn, ResponsibleColumn, @@ -41,7 +41,7 @@ function buildOrderTableColumns(): TableColumn[] { accessor: 'part', sortable: true, switchable: false, - render: (record: any) => + render: (record: any) => PartColumn(record.part_detail) }, { accessor: 'title', diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 5166469637..5d10c1484c 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react'; import { ActionButton } from '../../components/buttons/ActionButton'; import { AddItemButton } from '../../components/buttons/AddItemButton'; -import { Thumbnail } from '../../components/images/Thumbnail'; import ImporterDrawer from '../../components/importer/ImporterDrawer'; import { ProgressBar } from '../../components/items/ProgressBar'; import { RenderStockLocation } from '../../components/render/Stock'; @@ -30,6 +29,7 @@ import { CurrencyColumn, LinkColumn, NoteColumn, + PartColumn, ReferenceColumn, TargetDateColumn, TotalPriceColumn @@ -124,14 +124,7 @@ export function PurchaseOrderLineItemTable({ title: t`Internal Part`, sortable: true, switchable: false, - render: (record: any) => { - return ( - - ); - } + render: (record: any) => PartColumn(record.part_detail) }, { accessor: 'description', diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx new file mode 100644 index 0000000000..957a32cdb2 --- /dev/null +++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx @@ -0,0 +1,272 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { IconSquareArrowRight } from '@tabler/icons-react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; + +import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { ProgressBar } from '../../components/items/ProgressBar'; +import { formatCurrency } from '../../defaults/formatters'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms'; +import { + useCreateApiFormModal, + useDeleteApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { DateColumn, LinkColumn, PartColumn } from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { + RowDeleteAction, + RowDuplicateAction, + RowEditAction +} from '../RowActions'; +import { TableHoverCard } from '../TableHoverCard'; + +export default function SalesOrderLineItemTable({ + orderId, + customerId +}: { + orderId: number; + customerId: number; +}) { + const user = useUserState(); + const table = useTable('sales-order-line-item'); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'part', + sortable: true, + switchable: false, + render: (record: any) => PartColumn(record?.part_detail) + }, + { + accessor: 'part_detail.IPN', + title: t`IPN`, + switchable: true + }, + { + accessor: 'part_detail.description', + title: t`Description`, + sortable: false, + switchable: true + }, + { + accessor: 'reference', + sortable: false, + switchable: true + }, + { + accessor: 'quantity', + sortable: true + }, + { + accessor: 'sale_price', + render: (record: any) => + formatCurrency(record.sale_price, { + currency: record.sale_price_currency + }) + }, + { + accessor: 'total_price', + title: t`Total Price`, + render: (record: any) => + formatCurrency(record.sale_price, { + currency: record.sale_price_currency, + multiplier: record.quantity + }) + }, + DateColumn({ + accessor: 'target_date', + sortable: true, + title: t`Target Date` + }), + { + accessor: 'stock', + title: t`Available Stock`, + render: (record: any) => { + let part_stock = record?.available_stock ?? 0; + let variant_stock = record?.available_variant_stock ?? 0; + let available = part_stock + variant_stock; + + let required = Math.max( + record.quantity - record.allocated - record.shipped, + 0 + ); + + let color: string | undefined = undefined; + let text: string = `${available}`; + + let extra: ReactNode[] = []; + + if (available <= 0) { + color = 'red'; + text = t`No stock available`; + } else if (available < required) { + color = 'orange'; + } + + if (variant_stock > 0) { + extra.push({t`Includes variant stock`}); + } + + return ( + {text}} + extra={extra} + title={t`Stock Information`} + /> + ); + } + }, + { + accessor: 'allocated', + render: (record: any) => ( + + ) + }, + { + accessor: 'shipped', + render: (record: any) => ( + + ) + }, + { + accessor: 'notes' + }, + LinkColumn({ + accessor: 'link' + }) + ]; + }, []); + + const [selectedLine, setSelectedLine] = useState(0); + + const [initialData, setInitialData] = useState({}); + + const createLineFields = useSalesOrderLineItemFields({ + orderId: orderId, + customerId: customerId, + create: true + }); + + const newLine = useCreateApiFormModal({ + url: ApiEndpoints.sales_order_line_list, + title: t`Add Line Item`, + fields: createLineFields, + initialData: initialData, + table: table + }); + + const editLineFields = useSalesOrderLineItemFields({ + orderId: orderId, + customerId: customerId, + create: false + }); + + const editLine = useEditApiFormModal({ + url: ApiEndpoints.sales_order_line_list, + pk: selectedLine, + title: t`Edit Line Item`, + fields: editLineFields, + table: table + }); + + const deleteLine = useDeleteApiFormModal({ + url: ApiEndpoints.sales_order_line_list, + pk: selectedLine, + title: t`Delete Line Item`, + table: table + }); + + const tableActions = useMemo(() => { + return [ + { + setInitialData({ + order: orderId + }); + newLine.open(); + }} + hidden={!user.hasAddRole(UserRoles.sales_order)} + /> + ]; + }, [user]); + + const rowActions = useCallback( + (record: any) => { + const allocated = (record?.allocated ?? 0) > (record?.quantity ?? 0); + + return [ + { + hidden: allocated || !user.hasChangeRole(UserRoles.sales_order), + title: t`Allocate stock`, + icon: , + color: 'green' + }, + RowEditAction({ + hidden: !user.hasChangeRole(UserRoles.sales_order), + onClick: () => { + setSelectedLine(record.pk); + editLine.open(); + } + }), + RowDuplicateAction({ + hidden: !user.hasAddRole(UserRoles.sales_order), + onClick: () => { + setInitialData(record); + newLine.open(); + } + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.sales_order), + onClick: () => { + setSelectedLine(record.pk); + deleteLine.open(); + } + }) + ]; + }, + [user] + ); + + return ( + <> + {editLine.modal} + {deleteLine.modal} + {newLine.modal} + + + ); +}